commit c6d7af9ae4a0ea3dd7193cb4b73a0c2f93524760 Author: honghefly Date: Thu Nov 20 16:42:59 2025 +0800 first commit diff --git a/ICONS_README.md b/ICONS_README.md new file mode 100644 index 0000000..8e30499 --- /dev/null +++ b/ICONS_README.md @@ -0,0 +1,63 @@ +# 小程序图标说明 + +## ✅ 图标已创建 + +所有必需的图标文件已经创建在 `miniapp/images/` 目录中: + +### TabBar图标 +| 文件名 | 用途 | 颜色 | +|--------|------|------| +| home.png | 首页图标(未选中) | #999999 | +| home-active.png | 首页图标(选中) | #07c160 | +| stats.png | 统计图标(未选中) | #999999 | +| stats-active.png | 统计图标(选中) | #07c160 | +| profile.png | 个人中心(未选中) | #999999 | +| profile-active.png | 个人中心(选中) | #07c160 | + +### 其他图标 +| 文件名 | 用途 | +|--------|------| +| default-avatar.png | 默认头像 | +| empty.png | 空状态图片 | + +## 🎨 生成真实PNG图标 + +当前的PNG文件是占位符(1x1像素),可以让小程序正常运行。如需生成真实的图标: + +### 方法1:使用HTML生成器 +1. 在浏览器中打开 `miniapp/generate-icons.html` +2. 点击"下载所有图标"按钮 +3. 将下载的PNG文件覆盖到 `images/` 目录 + +### 方法2:使用SVG文件 +- `images/` 目录中包含了对应的SVG文件 +- 可以使用在线工具(如 https://cloudconvert.com/svg-to-png)转换为PNG + +### 方法3:使用图标库 +推荐使用以下图标: +- 🏠 首页: home, house +- 📊 统计: chart-bar, statistics +- 👤 个人: user, person + +## 📏 图标规范 + +- **尺寸**: 81x81 像素(微信小程序推荐) +- **格式**: PNG(支持透明背景) +- **颜色**: + - 未选中: #999999 + - 选中状态: #07c160(微信绿) + +## 🔧 自定义图标 + +如需替换为自定义图标: +1. 准备81x81像素的PNG图片 +2. 命名规则:`{name}.png` 和 `{name}-active.png` +3. 将文件放入 `images/` 目录 +4. 重新编译小程序 + +## 💡 提示 + +- 图标文件很小,不会影响小程序性能 +- 建议使用简洁的线性图标 +- 保持图标风格统一 +- 可以使用 iconfont 或其他图标网站下载 \ No newline at end of file diff --git a/MINIAPP_README.md b/MINIAPP_README.md new file mode 100644 index 0000000..33cea85 --- /dev/null +++ b/MINIAPP_README.md @@ -0,0 +1,172 @@ +# 打牌记账小程序 - 使用说明 + +## 🚀 快速开始 + +### 1. 环境准备 +- 微信开发者工具 +- 后端服务已启动(http://localhost:3000) + +### 2. 导入项目 +1. 打开微信开发者工具 +2. 导入项目,选择 `miniapp` 目录 +3. AppID 使用测试号或实际AppID + +### 3. 安装依赖 +```bash +cd miniapp +npm install +``` + +在微信开发者工具中: +- 工具 > 构建 npm + +### 4. 修改配置 +如果后端不在localhost:3000,修改: +- `app.js` 中的 `apiBase` +- `utils/request.js` 中的 `API_BASE` + +## 📱 功能说明 + +### 已实现功能 +1. **用户登录** - 首页点击登录按钮 +2. **首页展示** + - 用户信息卡片 + - 快速操作入口 + - 进行中的牌局列表 + +### 核心页面 +| 页面 | 路径 | 功能 | +|------|------|------| +| 首页 | pages/index/index | 登录、快速操作、牌局列表 | +| 创建牌局 | pages/game/create/create | 创建新牌局 | +| 加入牌局 | pages/game/join/join | 输入邀请码加入 | +| 牌局详情 | pages/game/detail/detail | 查看牌局信息 | +| 游戏进行 | pages/game/play/play | 记账功能 | +| 个人统计 | pages/stats/personal/personal | 战绩统计 | +| 个人中心 | pages/profile/index/index | 个人信息 | + +## 🎮 使用流程 + +### 1. 登录流程 +``` +打开小程序 → 首页 → 点击登录 → 授权用户信息 → 登录成功 +``` + +### 2. 创建牌局 +``` +首页 → 创建牌局 → 填写信息 → 生成邀请码 → 分享给朋友 +``` + +### 3. 加入牌局 +``` +首页 → 加入牌局 → 输入邀请码 → 加入成功 → 进入牌局 +``` + +### 4. 记账流程 +``` +牌局详情 → 开始游戏 → 记一笔 → 输入输赢金额 → 保存 +``` + +## 🔧 API调用示例 + +### 登录 +```javascript +const app = getApp() +await app.login() +``` + +### 创建牌局 +```javascript +import request from '../../utils/request' + +const data = await request.post('/sessions/create', { + session_name: '周五麻将', + game_type: 'mahjong', + base_score: 100, + player_count: 4 +}) +``` + +### 记录游戏 +```javascript +const data = await request.post('/records/add', { + session_id: 1, + dealer_id: 1, + player_records: [ + { player_id: 1, score_change: 300 }, + { player_id: 2, score_change: -100 }, + { player_id: 3, score_change: -100 }, + { player_id: 4, score_change: -100 } + ], + notes: '张三自摸' +}) +``` + +## 📝 注意事项 + +1. **登录状态** - Token保存在Storage,24小时有效 +2. **数据刷新** - onShow时自动刷新数据 +3. **错误处理** - 网络错误自动提示 +4. **金额单位** - 后端使用分,前端显示元 + +## 🐛 常见问题 + +### 1. 无法登录 +- 检查后端服务是否启动 +- 检查网络连接 +- 清除缓存重试 + +### 2. 请求失败 +- 检查API地址配置 +- 查看控制台错误信息 +- 验证Token是否过期 + +### 3. 页面空白 +- 重新构建npm +- 检查页面路径配置 +- 查看编译错误 + +## 📂 项目结构 +``` +miniapp/ +├── pages/ # 页面文件 +│ ├── index/ # 首页 +│ ├── game/ # 游戏相关 +│ │ ├── create/ # 创建牌局 +│ │ ├── join/ # 加入牌局 +│ │ ├── detail/ # 牌局详情 +│ │ └── play/ # 游戏进行 +│ ├── stats/ # 统计相关 +│ └── profile/ # 个人中心 +├── utils/ # 工具函数 +│ ├── request.js # 请求封装 +│ └── format.js # 格式化工具 +├── images/ # 图片资源 +├── app.js # 应用入口 +├── app.json # 应用配置 +└── app.wxss # 全局样式 +``` + +## 🎯 下一步开发 + +### 待实现功能 +1. 实时同步(WebSocket) +2. 结算功能 +3. 数据图表展示 +4. 语音记账 +5. 拍照记录 +6. 导出报表 + +### 优化建议 +1. 添加骨架屏 +2. 优化加载动画 +3. 添加下拉刷新 +4. 缓存优化 +5. 错误重试机制 + +## 📞 技术支持 + +如需帮助,请参考: +- [后端README](../BACKEND_README.md) +- [系统架构文档](../SYSTEM_ARCHITECTURE.md) +- [API设计文档](../API_DESIGN.md) \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..5cf032d --- /dev/null +++ b/README.md @@ -0,0 +1,253 @@ +## 安装/更新 + +1、终端使用命令安装 npm 包 + +```bash +npm install --save @jdwx/api@latest +``` + +> 一般会和[jdwx-components](https://code.miniappapi.com/wx/jdwx-components)一同搭配使用。安装: +> ```bash +> npm install --save @jdwx/components@latest +> ``` + +2、在小程序开发者工具中:菜单选择工具 -> 构建 npm + +## 使用 + +```js +import { + onLoginReady, + waitLogin, + + injectApp, + injectPage, + injectComponent, + hijackApp, + hijackAllPage, + + gatewayHttpClient, + baseHttpClient, + apiHttpClient, + HttpClient, + + adManager, +} from '@jdwx/api' +``` + +### `waitLogin`/`onLoginReady` - 确保登录完成 + +- 同步的写法 + +```js +async function onLoad() { + await waitLogin() + // 此处代码将在已登录或登陆完成后执行。请求将自动携带Token + await gatewayHttpClient.request('/xxx', 'GET', {}) +} +``` + +- 异步的写法 + +```js +function onLoad() { + onLoginReady(() => { + // 此处代码将在已登录或登陆完成后执行。请求将自动携带Token + gatewayHttpClient.request('/xxx', 'GET', {}) + }) +} +``` + +### `injectApp` - 向App注入基础代码 + +- 注入之后实现自动登录、广告初始化等功能 + +```js +// app.js +App(injectApp()({ + // 业务代码 + onLaunch() { + + } +})) +``` + +### `injectPage` - 向Page注入基础代码 + +- 注入之后实现页面自动统计、自动展示插屏广告以及激励视频广告的调用支持 +- 参数: + - showInterstitialAd: 是否自动展示插屏广告 + +```js +// pages/xxx/xxx.js +Page(injectPage({ + showInterstitialAd: true +})({ + // 业务代码 + onLoad() { + + } +})) +``` + +### `injectComponent` - 向Component注入基础代码 + +- 适用于使用Component构造页面的场景 +- 注入之后实现页面自动统计、自动展示插屏广告以及激励视频广告的调用支持 +- 参数: + - showInterstitialAd: 是否自动展示插屏广告 + +```js +// pages/xxx/xxx.js +Component(injectComponent({ + showInterstitialAd: true +})({ + // 业务代码 + methods: { + onLoad() { + + } + } +})) +``` + +### `hijackApp` - 劫持全局App方法,注入基础代码 + +- 在不方便使用injectApp时使用(如解包后代码复杂,难以修改App调用) +- 此方法会修改全局App方法,存在未知风险,使用时请进行完整测试 +- 不可与injectApp同时使用 + +```js +// app.js +hijackApp() +``` + +### `hijackAllPage` - 劫持全局Page方法,注入基础代码 + +- 在不方便使用injectPage/injectComponent时使用(如解包后代码复杂,难以修改Page/Component调用) +- 此方法会修改全局Page方法,并影响所有的页面,存在未知风险,使用时请进行完整测试 +- 参数同injectPage/injectComponent方法,不可与这些方法同时使用 + +```js +// app.js +hijackAllPage({ + showInterstitialAd: true +}) +``` + +### `gatewayHttpClient` - 网关API调用封装 + +- 同步的写法 + +```js +async function onLoad() { + try { + // 网关请求。参数:路径、方法、数据、其他选项(如headers、responseType) + const data = await gatewayHttpClient.request(path, method, data,options) + + // 头像上传。参数:文件路径 + const data = await gatewayHttpClient.uploadAvatar(filePath) + + // 文件上传。参数:文件路径、数据 + const data = await gatewayHttpClient.uploadFile(filePath, data) + + // 文件删除。参数:文件ID + const data = await gatewayHttpClient.deleteFile(fileId) + } catch(err) { + // 响应HTTP状态码非200时自动showToast并抛出异常 + } +} +``` + +- 所有方法均支持异步的写法 + +```js +function onLoad() { + gatewayHttpClient.request('/xxx') + .then(data => { + console.log(data) + }) + .catch(err => {}) +} +``` + +### `baseHttpClient`/`apiHttpClient` - 为老版本兼容保留,不推荐使用 + +### `HttpClient` - API底层类,用于封装自定义请求 + +- 示例:封装一个百度的请求客户端,并调用百度搜索 + +```js +const baiduHttpClient = new HttpClient({ + baseURL: 'https://www.baidu.com', + timeout: 5000, +}); + +baiduHttpClient.request('/s', 'GET', { wd: '测试' }, { responseType: 'text' }) + .then(data => console.log(data)) +``` + +### `adManager` - 广告管理器 + +- 确保广告数据加载完成,支持同步/异步的写法 + +```js +// 同步的写法 +async function onLoad() { + await adManager.waitAdData() + // 此处代码将在广告数据加载完成后执行 + await adManager.createAndShowInterstitialAd() +} + +// 异步的写法 +function onLoad () { + adManager.onDataReady(() => { + // 此处代码将在广告数据加载完成后执行 + adManager.createAndShowInterstitialAd() + }) +} +``` + +- 广告数据 + +```js +// 格式化之后的广告数据对象,如{banner: "adunit-f7709f349de05edc", custom: "adunit-34c76b0c3e4a6ed0", ...} +const ads = adManager.ads + +// 友情链接顶部广告数据 +const top = adManager.top + +// 友情链接数据 +const link = adManager.link +``` + +- 创建并展示插屏广告 + +```js +function onLoad() { + adManager.createAndShowInterstitialAd() +} +``` + +- 创建并展示激励视频广告 + - 传入当前页面的上下文this,返回用户是否已看完广告 + - 由于微信的底层限制,需要先在调用的页面上进行injectPage注入,且该方法必须放在用户的点击事件里调用 + - 使用示例可参考[jdwx-demo](https://code.miniappapi.com/wx/jdwx-demo) + +```js +// 同步的写法 +page({ + async handleClick() { + const isEnded = await adManager.createAndShowRewardedVideoAd(this) + } +}) + +// 异步的写法 +page({ + handleClick() { + adManager.createAndShowRewardedVideoAd(this).then((isEnded) => { + + }) + } +}) +``` \ No newline at end of file diff --git a/THEME_IMPLEMENTATION.md b/THEME_IMPLEMENTATION.md new file mode 100644 index 0000000..d684de0 --- /dev/null +++ b/THEME_IMPLEMENTATION.md @@ -0,0 +1,363 @@ +# Miniapp3 黑金奢华主题 - 实施完成报告 + +## 🎨 主题概览 + +**主题名称**: 黑金奢华 (Black & Gold Luxury) +**设计风格**: 高端、奢华、精致 +**核心理念**: 通过黑色基调和金色点缀营造尊贵感 + +--- + +## 🌟 核心配色方案 + +### 主色系 +- **金色主调**: `#D4AF37` → `#F4E6C3` (渐变) +- **金色深色**: `#B8941F` → `#9A7B1A` +- **银色辅助**: `#C0C0C0` → `#E5E5E5` + +### 背景色系 +- **纯黑背景**: `#000000` → `#0A0A0A` +- **卡片背景**: `#1A1A1A` → `#252525` +- **深色层级**: `#121212`, `#2A2A2A`, `#3A3A3A` + +### 文字色系 +- **主文本**: `#FFFFFF` (纯白) +- **次级文本**: `#E0E0E0` +- **辅助文本**: `#B0B0B0` +- **禁用文本**: `#606060` +- **金色文本**: `#F4E6C3` (用于标题) + +### 状态色 +- **成功**: `#50C878` (翡翠绿) +- **警告**: `#FFD700` (金黄) +- **错误**: `#DC143C` (深红) +- **信息**: `#4682B4` (钢蓝) + +--- + +## 📁 已完成的文件更新 + +### 1. 核心配置文件 + +#### [styles/theme.wxss](styles/theme.wxss) +完整的设计系统配置,包含: +- CSS 变量定义(颜色、阴影、圆角、间距) +- 渐变样式类(金色、银色、黑色) +- 文字颜色工具类 +- 背景颜色工具类 +- 阴影系统(带金色光晕) +- 边框样式(包括金色渐变边框) +- 按钮和卡片组件样式 +- 徽章系统 +- 特殊效果(金属质感、发光、玻璃态) +- 动画效果(淡入、滑入、发光脉冲、金属闪光) +- 响应式字体系统 +- 实用工具类 + +#### [app.wxss](app.wxss) +全局样式文件,包含: +- **按钮系统**: primary (金色), secondary (银色), success, warning, danger, ghost +- **卡片系统**: 标准卡片、金色装饰卡片、高亮卡片 +- **游戏相关**: 胜利、失败、平局样式 +- **列表样式**: 带悬停效果的列表项 +- **头像系统**: 4 种尺寸,金色边框,带发光效果 +- **徽章标签**: 金色、银色、成功、警告、危险 +- **模态框**: 带金色边框的弹窗系统 +- **空状态**: 统一的空状态展示 +- **加载状态**: 金色加载指示器 +- **底部操作栏**: 固定底栏,金色顶边 +- **排名徽章**: 金银铜三色渐变徽章 +- **分割线**: 普通分割线和金色渐变分割线 +- **实用工具类**: margin, padding, text-align + +### 2. 组件样式 + +#### [components/navbar/navbar.wxss](components/navbar/navbar.wxss) +- 黑色渐变背景 (#1A1A1A → #252525) +- 金色底边装饰 +- 深黑阴影 +- 返回按钮:金色半透明背景,金色边框 +- 标题:浅金色 (#F4E6C3) + +#### [components/custom-tabbar/custom-tabbar.wxss](components/custom-tabbar/custom-tabbar.wxss) +- 黑色背景 (#1A1A1A) +- 金色顶边装饰 +- 深黑阴影 +- 未选中标签:灰色 (#B0B0B0) +- 选中标签:金色 (#D4AF37),带放大和上移效果 + +#### [components/guide/guide.wxss](components/guide/guide.wxss) +- 遮罩:85% 黑色半透明 +- 提示卡片:黑色渐变背景,金色边框 +- 标题:浅金色 (#F4E6C3) +- 描述:浅灰色 (#E0E0E0) +- 进度点: + - 未激活:深灰 (#3A3A3A) + - 激活:金色渐变,带发光效果 +- 按钮:金色渐变,黑色文字 + +### 3. 页面样式 + +#### [pages/index/index.wxss](pages/index/index.wxss) +**增强功能**: +1. **用户卡片** + - 黑色渐变背景 + - 金色边框装饰 + - 顶部金色渐变条(带闪光动画) + - 深黑阴影 + +2. **用户头像** + - 金色边框 (3rpx) + - 金色发光效果 + - 呼吸式发光动画 (avatarGlow) + +3. **统计数据** + - 金色顶边分割线 + - 白色数值文字 + - 灰色标签文字 + +4. **操作按钮** + - 主按钮:绿色渐变,带发光阴影 + - 次要按钮:银色渐变,带柔和阴影 + - 悬停:阴影加深效果 + +5. **房间卡片** + - 黑色背景 (#1A1A1A) + - 深黑阴影 + - 房主徽章:橙色半透明 + - 状态徽章: + - 进行中:翡翠绿 + - 等待中:橙色 + - 时间文本:灰色 + +6. **空状态** + - 黑色背景 + - 深黑阴影 + - 灰色文字 + +--- + +## ✨ 关键视觉特征 + +### 1. 金属质感效果 +```css +.gradient-metallic-gold { + background: linear-gradient(135deg, + #B8941F 0%, + #D4AF37 25%, + #F4E6C3 50%, + #D4AF37 75%, + #B8941F 100% + ); + background-size: 200% 200%; + animation: shimmer 3s linear infinite; +} +``` + +### 2. 发光效果 +```css +--glow-gold: 0 0 20rpx rgba(212, 175, 55, 0.6), + 0 0 40rpx rgba(212, 175, 55, 0.4), + 0 0 60rpx rgba(212, 175, 55, 0.2); +``` + +### 3. 动画系统 +- **shimmer** - 金属闪光动画 (4s) +- **avatarGlow** - 头像呼吸发光 (3s) +- **fadeInDark** - 淡入效果 +- **slideInGold** - 金色滑入 +- **glowPulse** - 发光脉冲 + +### 4. 阴影系统 +```css +--shadow-sm: 0 2rpx 8rpx rgba(0, 0, 0, 0.6); +--shadow-md: 0 4rpx 16rpx rgba(0, 0, 0, 0.7); +--shadow-lg: 0 8rpx 32rpx rgba(0, 0, 0, 0.8); +--shadow-gold: 0 4rpx 20rpx rgba(212, 175, 55, 0.4); +--shadow-gold-lg: 0 8rpx 32rpx rgba(212, 175, 55, 0.5); +``` + +--- + +## 🎯 设计原则 + +### 1. 奢华感营造 +- **金色点缀**: 边框、图标、高亮文字使用金色 +- **黑色基调**: 深黑背景营造高端氛围 +- **发光效果**: 金色元素带有柔和发光 +- **金属质感**: 渐变和动画模拟金属光泽 + +### 2. 视觉层次 +- **主要内容**: 白色文字 (#FFFFFF) +- **次要内容**: 浅灰文字 (#E0E0E0) +- **辅助内容**: 灰色文字 (#B0B0B0) +- **金色强调**: 标题和重要元素 (#F4E6C3) + +### 3. 交互反馈 +- **悬停**: transform scale + 阴影变化 +- **激活**: 阴影加深 + 轻微缩放 +- **动画**: 流畅的 cubic-bezier 过渡 +- **发光**: 金色元素呼吸式发光 + +### 4. 品牌一致性 +- **主题色**: 金色代表尊贵 +- **辅助色**: 银色代表精致 +- **背景色**: 纯黑代表高端 +- **强调色**: 翡翠绿代表成功 + +--- + +## 📊 更新统计 + +### 文件修改数量 +- **核心配置**: 2 个文件 (theme.wxss, app.wxss) +- **组件样式**: 3 个文件 (navbar, tabbar, guide) +- **页面样式**: 1 个文件 (index.wxss) +- **批量更新**: 21+ 个 wxss 文件 + +### 样式更新内容 +- **颜色替换**: 100+ 处 + - 紫色 → 金色/黑色 + - 白色背景 → 黑色背景 + - 深色文字 → 浅色文字 +- **阴影更新**: 50+ 处 + - 紫色阴影 → 黑色/金色阴影 +- **新增效果**: 20+ 个 + - 发光动画 + - 金属闪光 + - 渐变边框 + +--- + +## 🔧 技术实现细节 + +### 渐变方向统一 +所有渐变使用 **135deg** 方向,确保视觉一致性 + +### 圆角系统 +```css +--radius-sm: 8rpx; +--radius-md: 12rpx; +--radius-lg: 16rpx; +--radius-xl: 20rpx; +--radius-full: 9999rpx; +``` + +### 间距系统 +```css +--space-xs: 8rpx; +--space-sm: 12rpx; +--space-md: 16rpx; +--space-lg: 24rpx; +--space-xl: 32rpx; +``` + +### 动画性能优化 +- 使用 `will-change` 提示浏览器优化 +- 避免在 `width`/`height` 上使用动画 +- 优先使用 `transform` 和 `opacity` +- 合理控制动画时长 (2-4s) + +--- + +## 🚀 使用指南 + +### 1. 应用主题样式 +在页面 wxss 中引入主题: +```css +@import "../../styles/theme.wxss"; +``` + +### 2. 使用预定义类 +```html + + + + + + + 金色装饰卡片 + + + +VIP + + + + + +金色文字 +辅助文字 +``` + +### 3. 自定义组件样式 +参考 `theme.wxss` 中的 CSS 变量: +```css +.my-component { + background: var(--bg-card); + color: var(--text-primary); + border: 1rpx solid var(--border-gold); + box-shadow: var(--shadow-gold); + border-radius: var(--radius-lg); +} +``` + +--- + +## 💡 设计理念对比 + +| 特征 | Miniapp (紫色) | Miniapp2 (橙色) | Miniapp3 (黑金) | +|------|---------------|----------------|----------------| +| **定位** | 神秘科技 | 清新友好 | 奢华高端 | +| **主色** | #667eea 紫色 | #FF6B35 橙色 | #D4AF37 金色 | +| **背景** | 渐变紫色 | 浅橙白色 | 纯黑色 | +| **风格** | 现代炫酷 | 温暖活泼 | 精致尊贵 | +| **用户群** | 年轻科技爱好者 | 休闲游戏玩家 | 高端用户 | +| **特效** | 霓虹发光 | 柔和阴影 | 金属光泽 | + +--- + +## ✅ 实施检查清单 + +- [x] 主题配置文件创建完成 +- [x] 全局样式文件创建完成 +- [x] Navbar 组件金色样式完成 +- [x] TabBar 组件金色样式完成 +- [x] Guide 组件金色样式完成 +- [x] 首页样式增强完成 +- [x] 按钮系统金色/银色渐变 +- [x] 卡片系统金色装饰 +- [x] 文字颜色浅色适配 +- [x] 背景颜色深色统一 +- [x] 阴影效果深黑/金色 +- [x] 发光效果添加 +- [x] 动画效果优化 +- [x] 响应式适配完成 + +--- + +## 📞 后续优化建议 + +1. **性能优化** + - 减少动画使用频率 + - 优化大图片加载 + - 使用 CSS containment + +2. **无障碍改进** + - 确保颜色对比度 ≥ 4.5:1 + - 添加 aria 标签 + - 支持高对比度模式 + +3. **深色模式增强** + - 添加"纯黑"和"深灰"两种模式切换 + - 保存用户偏好设置 + +4. **主题扩展** + - 支持自定义金色色相 + - 添加更多预设主题配色 + +--- + +**更新日期**: 2025-01-XX +**版本**: v3.0.0 +**主题**: 黑金奢华 (Black & Gold Luxury) diff --git a/USER_GUIDE_README.md b/USER_GUIDE_README.md new file mode 100644 index 0000000..1db3208 --- /dev/null +++ b/USER_GUIDE_README.md @@ -0,0 +1,432 @@ +# 新用户引导功能说明 + +## 功能概述 + +为首页添加了新用户引导功能,首次访问小程序时会自动显示,帮助用户快速了解核心功能。 + +--- + +## 技术实现 + +### 1. 引导组件 (`components/guide/guide`) + +**核心特性**: +- ✅ 高亮遮罩层(打洞效果) +- ✅ 自适应位置计算 +- ✅ 多步骤引导流程 +- ✅ 进度指示器 +- ✅ 平滑过渡动画 +- ✅ 防止穿透交互 + +**文件结构**: +``` +components/guide/ +├── guide.wxml # 模板(遮罩 + 高亮 + 提示卡片) +├── guide.wxss # 样式(渐变背景 + 动画) +├── guide.js # 逻辑(位置计算 + 步骤控制) +└── guide.json # 配置 +``` + +### 2. 引导步骤配置 + +在 [pages/index/index.js](pages/index/index.js) 中定义引导步骤: + +```javascript +guideSteps: [ + { + selector: '.guide-create-btn', // 目标元素选择器 + title: '创建房间', // 标题 + description: '点击这里可以...', // 说明文字 + position: 'bottom', // 提示框位置(top/bottom/left/right) + padding: 12, // 高亮区域扩展边距 + borderRadius: 16 // 高亮区域圆角 + }, + // ... 更多步骤 +] +``` + +### 3. 首次访问检测 + +使用 `wx.getStorageSync('hasShownGuide')` 检测是否已显示过引导: + +```javascript +checkAndShowGuide() { + const hasShownGuide = wx.getStorageSync('hasShownGuide') + if (!hasShownGuide) { + // 延迟500ms显示,等待页面完全渲染 + setTimeout(() => { + this.setData({ showGuide: true }) + }, 500) + } +} +``` + +### 4. 目标元素标记 + +为需要引导的元素添加唯一的class: + +```html + + + + + + + + + + + + + + +``` + +--- + +## 使用方式 + +### 在其他页面中使用 + +#### 1. 注册组件 + +在页面的 `xxx.json` 中: + +```json +{ + "usingComponents": { + "user-guide": "../../components/guide/guide" + } +} +``` + +#### 2. 添加模板 + +在页面的 `xxx.wxml` 中: + +```html + +``` + +#### 3. 配置步骤 + +在页面的 `xxx.js` 中: + +```javascript +Page({ + data: { + showGuide: false, + guideSteps: [ + { + selector: '.target-element', + title: '功能标题', + description: '功能说明文字', + position: 'bottom', // 可选:top/bottom/left/right + padding: 12, // 可选:高亮区域扩展边距(默认8) + borderRadius: 16, // 可选:圆角大小(默认16) + showArrow: true // 可选:是否显示箭头(默认true) + } + // ... 更多步骤 + ] + }, + + onReady() { + // 检查是否需要显示引导 + const hasShown = wx.getStorageSync('hasShownGuide_pageName') + if (!hasShown) { + setTimeout(() => { + this.setData({ showGuide: true }) + }, 500) + } + }, + + onGuideComplete() { + this.setData({ showGuide: false }) + wx.setStorageSync('hasShownGuide_pageName', true) + } +}) +``` + +--- + +## 测试方法 + +### 方法1:清除存储测试 + +在微信开发者工具的控制台中执行: + +```javascript +// 清除引导标记 +wx.removeStorageSync('hasShownGuide') + +// 重新加载页面 +wx.reLaunch({ url: '/pages/index/index' }) +``` + +### 方法2:清除缓存测试 + +1. 微信开发者工具 → 清缓存 → 清除所有 +2. 重新编译项目 +3. 重新进入首页 + +### 方法3:手动触发测试 + +在 [pages/index/index.js](pages/index/index.js) 中临时修改: + +```javascript +checkAndShowGuide() { + // 注释掉检查逻辑,直接显示 + // const hasShownGuide = wx.getStorageSync('hasShownGuide') + // if (!hasShownGuide) { + setTimeout(() => { + this.setData({ showGuide: true }) + }, 500) + // } +} +``` + +--- + +## 自定义配置 + +### 修改引导步骤 + +编辑 [pages/index/index.js](pages/index/index.js) 中的 `guideSteps` 数组: + +```javascript +guideSteps: [ + // 添加新的步骤 + { + selector: '.my-element', + title: '新功能', + description: '这是一个新功能的说明', + position: 'top', + padding: 10, + borderRadius: 12 + } +] +``` + +### 修改样式 + +编辑 [components/guide/guide.wxss](components/guide/guide.wxss): + +```css +/* 修改遮罩透明度 */ +.guide-overlay { + background: rgba(0, 0, 0, 0.75); /* 调整透明度 */ +} + +/* 修改提示卡片样式 */ +.guide-text-wrapper { + background: white; /* 背景色 */ + border-radius: 16rpx; /* 圆角 */ + padding: 32rpx 28rpx; /* 内边距 */ +} + +/* 修改按钮样式 */ +.guide-next { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); +} +``` + +### 修改位置计算逻辑 + +编辑 [components/guide/guide.js](components/guide/guide.js) 中的 `calculateContentPosition` 方法。 + +--- + +## 功能特点 + +### 1. 智能位置计算 + +组件会自动计算最佳显示位置,避免超出屏幕边界: + +- 优先使用指定位置(top/bottom/left/right) +- 空间不足时自动切换到对面 +- 保持在屏幕可视范围内 + +### 2. 高亮打洞效果 + +使用 CSS `box-shadow` 实现高亮区域: + +```css +.guide-spotlight { + box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.75); +} +``` + +这样可以在遮罩层上"打洞",突出显示目标元素。 + +### 3. 步骤进度指示 + +底部圆点显示当前进度: + +- 灰色圆点:未完成的步骤 +- 紫色长条:当前步骤 +- 平滑过渡动画 + +### 4. 防止交互穿透 + +```javascript +// 阻止滚动 +preventMove() {} + +// 阻止点击穿透 +catchtouchmove="preventMove" +``` + +--- + +## 注意事项 + +### 1. 选择器必须唯一 + +确保每个引导步骤的 `selector` 能唯一定位到目标元素: + +```html + + + + + + +``` + +### 2. 等待页面渲染 + +在 `onReady` 生命周期中显示引导,并使用延迟: + +```javascript +onReady() { + setTimeout(() => { + this.setData({ showGuide: true }) + }, 500) // 延迟500ms,确保页面完全渲染 +} +``` + +### 3. 避免在隐藏元素上使用 + +不要在 `wx:if="{{false}}"` 或 `display: none` 的元素上使用引导,否则无法获取位置。 + +### 4. 不同页面使用不同标记 + +如果多个页面都有引导,使用不同的storage key: + +```javascript +// 首页 +wx.setStorageSync('hasShownGuide_index', true) + +// 其他页面 +wx.setStorageSync('hasShownGuide_detail', true) +``` + +--- + +## API参数 + +### 组件属性 + +| 属性 | 类型 | 必需 | 默认值 | 说明 | +|------|------|------|--------|------| +| show | Boolean | 是 | false | 是否显示引导 | +| steps | Array | 是 | [] | 引导步骤配置 | + +### 组件事件 + +| 事件 | 参数 | 说明 | +|------|------|------| +| complete | 无 | 引导完成时触发 | + +### 步骤配置 + +| 字段 | 类型 | 必需 | 默认值 | 说明 | +|------|------|------|--------|------| +| selector | String | 是 | - | 目标元素的CSS选择器 | +| title | String | 是 | - | 引导标题 | +| description | String | 是 | - | 引导说明文字 | +| position | String | 否 | 'bottom' | 提示框位置(top/bottom/left/right)| +| padding | Number | 否 | 8 | 高亮区域扩展边距(px)| +| borderRadius | Number | 否 | 16 | 高亮区域圆角大小(px)| +| showArrow | Boolean | 否 | true | 是否显示箭头 | + +--- + +## 效果预览 + +### 步骤1:创建房间 +- 高亮"创建房间"按钮 +- 显示说明文字 +- 进度指示:1/3 + +### 步骤2:加入房间 +- 高亮"加入房间"按钮 +- 显示说明文字 +- 进度指示:2/3 + +### 步骤3:扫码进入 +- 高亮"扫码"按钮 +- 显示说明文字 +- 进度指示:3/3 +- 显示"知道了"按钮 + +--- + +## 常见问题 + +### Q: 引导没有显示? + +A: 检查以下几点: +1. 是否已经显示过(清除 storage) +2. 选择器是否正确 +3. 目标元素是否已渲染 +4. 延迟时间是否足够 + +### Q: 位置显示不对? + +A: 检查: +1. 目标元素是否被隐藏 +2. 页面是否完全渲染 +3. 增加 `setTimeout` 延迟时间 + +### Q: 如何调试? + +A: 在组件的 `showStep` 方法中添加日志: + +```javascript +showStep(stepIndex) { + const step = this.data.steps[stepIndex] + console.log('显示引导步骤:', stepIndex, step) + // ... +} +``` + +### Q: 如何强制重新显示引导? + +A: 控制台执行: + +```javascript +wx.removeStorageSync('hasShownGuide') +getCurrentPages()[0].setData({ showGuide: true }) +``` + +--- + +## 未来优化方向 + +1. **手势控制** - 支持滑动切换步骤 +2. **视频教程** - 嵌入视频演示 +3. **自定义主题** - 支持暗色模式 +4. **动画增强** - 添加更多过渡动画 +5. **智能触发** - 根据用户行为动态显示引导 +6. **A/B测试** - 支持多种引导方案 + +--- + +## 开发者 + +如有问题或建议,请联系开发团队。 diff --git a/app.js b/app.js new file mode 100644 index 0000000..69784a8 --- /dev/null +++ b/app.js @@ -0,0 +1,148 @@ +import { injectApp, waitLogin, gatewayHttpClient } from '@jdmini/api' +import request from './utils/request' + +App(injectApp()({ + globalData: { + userInfo: null, + currentSession: null, + openid: null, + jdToken: null + }, + + async getQrcode(scene, page = 'pages/game/join/join') { + try { + const response = await gatewayHttpClient.request('/wx/v1/api/app/qrcode', 'POST', { + scene: scene, + page: page, + check_path: false, // 关闭路径检查,因为可能是动态路由 + env_version: 'release' // 开发版本 + }, + { + responseType: 'arraybuffer' + } + ) + console.log('二维码API响应:', response) + // 检查响应是否是对象,如果是则可能包含data字段 + if (response && typeof response === 'object' && response.data) { + return response.data + } + return response + } catch (error) { + console.error('获取二维码失败:', error) + throw error + } + }, + + async onLaunch() { + console.log('App启动') + + // 等待JD登录完成,获取openid + await this.initJDLogin() + + // JD登录完成后,尝试后端登录 + //await this.loginToBackend() + // console.log(await this.getQrcode('a=1&b=2')) + }, + + // 初始化JD登录,获取openid和token + async initJDLogin() { + try { + // 等待JD登录完成 + await waitLogin() + + // 获取JD用户信息 + const jdToken = wx.getStorageSync('jdwx-token') + const jdUserInfo = wx.getStorageSync('jdwx-userinfo') + + if (jdUserInfo && jdUserInfo.openId) { + this.globalData.openid = jdUserInfo.openId + this.globalData.jdToken = jdToken + console.log('JD登录成功,openid:', jdUserInfo.openId) + } + } catch (error) { + console.error('JD登录失败:', error) + } + }, + + // 登录到后端 + async loginToBackend() { + try { + if (!this.globalData.openid) { + console.log('没有openid,跳过后端登录') + return false + } + + console.log('尝试后端登录,openid:', this.globalData.openid) + + // 调用后端登录接口(不传userInfo,只用openid查询) + const response = await request.post('/auth/login', { + openid: this.globalData.openid + }) + + if (response && response.player) { + // 用户已注册,保存用户信息 + this.globalData.userInfo = response.player + wx.setStorageSync('userInfo', response.player) + console.log('后端登录成功,用户信息:', response.player) + + // 触发登录成功事件,通知首页更新 + this.triggerLoginSuccess() + return true + } else if (response && response.exists === false) { + // 用户未注册(后端返回 player: null, exists: false) + console.log('用户未注册,需要完善信息') + return false + } else { + // 其他情况 + console.log('登录响应异常:', response) + return false + } + } catch (error) { + console.error('后端登录失败:', error) + return false + } + }, + + // 触发登录成功事件 + triggerLoginSuccess() { + this.refreshIndexPage() + }, + + // 刷新首页数据 + refreshIndexPage() { + // 获取所有页面 + const pages = getCurrentPages() + if (pages.length > 0) { + // 查找首页 + const indexPage = pages.find(page => page.route === 'pages/index/index') + if (indexPage && typeof indexPage.loadUserInfo === 'function') { + console.log('触发首页刷新') + indexPage.loadUserInfo() + } + } + }, + + // 获取当前用户信息 + getUserInfo() { + return wx.getStorageSync('userInfo') + }, + getUserOpenid() { + return this.globalData.openid + }, + + // 设置用户信息 + setUserInfo(userInfo) { + this.globalData.userInfo = userInfo + wx.setStorageSync('userInfo', userInfo) + }, + + // 更新当前牌局 + setCurrentSession(session) { + this.globalData.currentSession = session + }, + + // 获取当前牌局 + getCurrentSession() { + return this.globalData.currentSession + } +})) diff --git a/app.json b/app.json new file mode 100644 index 0000000..2498218 --- /dev/null +++ b/app.json @@ -0,0 +1,44 @@ +{ + "pages": [ + "pages/index/index", + "pages/login/login", + "pages/game/create/create", + "pages/game/join/join", + "pages/game/detail/detail", + "pages/game/settlement/settlement", + "pages/game/play/play", + "pages/record/list/list", + "pages/stats/personal/personal", + "pages/stats/session/session", + "pages/profile/index/index" + ], + "window": { + "navigationBarTextStyle": "black", + "navigationBarTitleText": "打牌记账小本", + "navigationBarBackgroundColor": "#07c160" + }, + "tabBar": { + "custom": true, + "color": "#666666", + "selectedColor": "#667eea", + "backgroundColor": "#ffffff", + "list": [ + { + "pagePath": "pages/index/index", + "text": "首页" + }, + { + "pagePath": "pages/stats/personal/personal", + "text": "统计" + }, + { + "pagePath": "pages/profile/index/index", + "text": "我的" + } + ] + }, + "style": "v2", + "componentFramework": "glass-easel", + "sitemapLocation": "sitemap.json", + "lazyCodeLoading": "requiredComponents" +} diff --git a/app.wxss b/app.wxss new file mode 100644 index 0000000..f64c9b8 --- /dev/null +++ b/app.wxss @@ -0,0 +1,527 @@ +/** + * h@7 - bN; + * ;rr (#D4AF37) + r (#000000) + * <bN + */ + +/* e;Mn */ +@import "styles/theme.wxss"; + +/* ==================== h@@7 ==================== */ + +page { + background: linear-gradient(180deg, #000000 0%, #0A0A0A 100%); + color: #FFFFFF; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + min-height: 100vh; +} + +/* ==================== ==================== */ + +/* ; - r */ +.btn-primary { + background: linear-gradient(135deg, #D4AF37 0%, #F4E6C3 100%); + color: #000000; + font-weight: 600; + border: none; + border-radius: 12rpx; + padding: 24rpx 48rpx; + font-size: 28rpx; + box-shadow: 0 8rpx 24rpx rgba(212, 175, 55, 0.4); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.btn-primary:active { + transform: scale(0.98); + box-shadow: 0 4rpx 16rpx rgba(212, 175, 55, 0.5); +} + +/* ! - r */ +.btn-secondary { + background: linear-gradient(135deg, #C0C0C0 0%, #E5E5E5 100%); + color: #000000; + font-weight: 600; + border: none; + border-radius: 12rpx; + padding: 24rpx 48rpx; + font-size: 28rpx; + box-shadow: 0 8rpx 24rpx rgba(192, 192, 192, 0.3); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.btn-secondary:active { + transform: scale(0.98); + box-shadow: 0 4rpx 16rpx rgba(192, 192, 192, 0.4); +} + +/*  - r */ +.btn-success { + background: linear-gradient(135deg, #50C878 0%, #06a854 100%); + color: #FFFFFF; + font-weight: 600; + border: none; + border-radius: 12rpx; + padding: 24rpx 48rpx; + font-size: 28rpx; + box-shadow: 0 8rpx 24rpx rgba(80, 200, 120, 0.3); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.btn-success:active { + transform: scale(0.98); + box-shadow: 0 4rpx 16rpx rgba(80, 200, 120, 0.4); +} + +/* fJ - r */ +.btn-warning { + background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%); + color: #000000; + font-weight: 600; + border: none; + border-radius: 12rpx; + padding: 24rpx 48rpx; + font-size: 28rpx; + box-shadow: 0 8rpx 24rpx rgba(255, 215, 0, 0.3); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.btn-warning:active { + transform: scale(0.98); + box-shadow: 0 4rpx 16rpx rgba(255, 215, 0, 0.4); +} + +/* qi - r */ +.btn-danger { + background: linear-gradient(135deg, #DC143C 0%, #8B0000 100%); + color: #FFFFFF; + font-weight: 600; + border: none; + border-radius: 12rpx; + padding: 24rpx 48rpx; + font-size: 28rpx; + box-shadow: 0 8rpx 24rpx rgba(220, 20, 60, 0.3); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.btn-danger:active { + transform: scale(0.98); + box-shadow: 0 4rpx 16rpx rgba(220, 20, 60, 0.4); +} + +/* }u - rF */ +.btn-ghost { + background: transparent; + color: #D4AF37; + border: 2rpx solid #D4AF37; + border-radius: 12rpx; + padding: 24rpx 48rpx; + font-size: 28rpx; + font-weight: 600; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.btn-ghost:active { + background: rgba(212, 175, 55, 0.1); + transform: scale(0.98); +} + +/* ( */ +.btn-disabled, +button[disabled] { + background: #2A2A2A !important; + color: #606060 !important; + box-shadow: none !important; + opacity: 0.5; +} + +/* ==================== aG ==================== */ + +/* aG */ +.card { + background: linear-gradient(135deg, #1A1A1A 0%, #252525 100%); + border-radius: 20rpx; + padding: 32rpx; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.8); + border: 1rpx solid rgba(212, 175, 55, 0.2); + margin-bottom: 24rpx; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.card:active { + transform: translateY(-2rpx); + box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.9); +} + +/* rpaG */ +.card-gold { + position: relative; + overflow: hidden; +} + +.card-gold::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3rpx; + background: linear-gradient(90deg, transparent 0%, #D4AF37 20%, #F4E6C3 50%, #D4AF37 80%, transparent 100%); +} + +/* خaG */ +.card-highlight { + background: linear-gradient(135deg, #252525 0%, #2A2A2A 100%); + border: 2rpx solid #D4AF37; + box-shadow: 0 4rpx 20rpx rgba(212, 175, 55, 0.3); +} + +/* ==================== 8s ==================== */ + +/* )7 */ +.game-win { + color: #50C878; + font-weight: bold; +} + +.game-win-bg { + background: linear-gradient(135deg, rgba(80, 200, 120, 0.15) 0%, rgba(6, 168, 84, 0.2) 100%); + border: 1rpx solid rgba(80, 200, 120, 0.3); +} + +/* 1%7 */ +.game-lose { + color: #DC143C; + font-weight: bold; +} + +.game-lose-bg { + background: linear-gradient(135deg, rgba(220, 20, 60, 0.15) 0%, rgba(139, 0, 0, 0.2) 100%); + border: 1rpx solid rgba(220, 20, 60, 0.3); +} + +/* s@7 */ +.game-draw { + color: #B0B0B0; + font-weight: bold; +} + +.game-draw-bg { + background: linear-gradient(135deg, rgba(176, 176, 176, 0.15) 0%, rgba(96, 96, 96, 0.2) 100%); + border: 1rpx solid rgba(176, 176, 176, 0.3); +} + +/* ==================== h7 ==================== */ + +.list-item { + background: #1A1A1A; + border-radius: 16rpx; + padding: 24rpx; + margin-bottom: 16rpx; + display: flex; + align-items: center; + border: 1rpx solid #2A2A2A; + transition: all 0.3s ease; +} + +.list-item:active { + background: #222222; + transform: translateX(8rpx); +} + +.list-item-content { + flex: 1; + margin-left: 20rpx; +} + +.list-item-title { + font-size: 28rpx; + font-weight: bold; + color: #FFFFFF; + margin-bottom: 8rpx; +} + +.list-item-desc { + font-size: 24rpx; + color: #B0B0B0; +} + +/* ==================== 4 ==================== */ + +.avatar { + border-radius: 50%; + background: linear-gradient(135deg, #2A2A2A 0%, #3A3A3A 100%); + border: 3rpx solid #D4AF37; + box-shadow: 0 4rpx 12rpx rgba(212, 175, 55, 0.4); +} + +.avatar-sm { + width: 60rpx; + height: 60rpx; +} + +.avatar-md { + width: 80rpx; + height: 80rpx; +} + +.avatar-lg { + width: 120rpx; + height: 120rpx; +} + +.avatar-xl { + width: 160rpx; + height: 160rpx; +} + +/* ==================== ~ ==================== */ + +.badge { + display: inline-flex; + align-items: center; + padding: 6rpx 16rpx; + border-radius: 24rpx; + font-size: 20rpx; + font-weight: 600; + border: 1rpx solid; +} + +.badge-gold { + background: linear-gradient(135deg, rgba(212, 175, 55, 0.15) 0%, rgba(244, 230, 195, 0.2) 100%); + color: #D4AF37; + border-color: #D4AF37; +} + +.badge-silver { + background: rgba(192, 192, 192, 0.15); + color: #C0C0C0; + border-color: #C0C0C0; +} + +.badge-success { + background: rgba(80, 200, 120, 0.15); + color: #50C878; + border-color: #50C878; +} + +.badge-warning { + background: rgba(255, 215, 0, 0.15); + color: #FFD700; + border-color: #FFD700; +} + +.badge-danger { + background: rgba(220, 20, 60, 0.15); + color: #DC143C; + border-color: #DC143C; +} + +/* ==================== !F/9 ==================== */ + +.modal-mask { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.85); + z-index: 9998; +} + +.modal-content { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 600rpx; + max-width: 90%; + background: linear-gradient(135deg, #1A1A1A 0%, #252525 100%); + border: 1rpx solid rgba(212, 175, 55, 0.3); + border-radius: 24rpx; + padding: 48rpx 32rpx 32rpx; + box-shadow: 0 16rpx 64rpx rgba(0, 0, 0, 0.9); + z-index: 9999; +} + +.modal-title { + font-size: 36rpx; + font-weight: bold; + color: #F4E6C3; + text-align: center; + margin-bottom: 24rpx; +} + +.modal-body { + font-size: 28rpx; + color: #E0E0E0; + line-height: 1.6; + margin-bottom: 32rpx; +} + +.modal-footer { + display: flex; + gap: 20rpx; +} + +.modal-footer button { + flex: 1; +} + +/* ==================== z ==================== */ + +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 100rpx 40rpx; + text-align: center; +} + +.empty-icon { + font-size: 120rpx; + color: #3A3A3A; + margin-bottom: 32rpx; + opacity: 0.6; +} + +.empty-text { + font-size: 28rpx; + color: #B0B0B0; + margin-bottom: 16rpx; +} + +.empty-hint { + font-size: 24rpx; + color: #606060; + margin-bottom: 32rpx; +} + +/* ==================== } ==================== */ + +.loading-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 80rpx 0; +} + +.loading-spinner { + width: 60rpx; + height: 60rpx; + border: 4rpx solid #2A2A2A; + border-top-color: #D4AF37; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.loading-text { + margin-top: 24rpx; + font-size: 24rpx; + color: #B0B0B0; +} + +/* ==================== \ ==================== */ + +.bottom-bar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background: #1A1A1A; + border-top: 1rpx solid rgba(212, 175, 55, 0.2); + padding: 24rpx 32rpx; + padding-bottom: calc(24rpx + env(safe-area-inset-bottom)); + box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.8); + z-index: 999; +} + +.bottom-bar-content { + display: flex; + gap: 20rpx; + align-items: center; +} + +/* ==================== ==================== */ + +.rank-badge { + display: inline-flex; + align-items: center; + justify-content: center; + width: 60rpx; + height: 60rpx; + border-radius: 50%; + font-size: 28rpx; + font-weight: bold; + border: 3rpx solid; +} + +.rank-1 { + background: linear-gradient(135deg, #D4AF37 0%, #F4E6C3 100%); + color: #000000; + border-color: #D4AF37; + box-shadow: 0 4rpx 16rpx rgba(212, 175, 55, 0.5); +} + +.rank-2 { + background: linear-gradient(135deg, #C0C0C0 0%, #E5E5E5 100%); + color: #000000; + border-color: #C0C0C0; + box-shadow: 0 4rpx 16rpx rgba(192, 192, 192, 0.4); +} + +.rank-3 { + background: linear-gradient(135deg, #CD7F32 0%, #E5A35C 100%); + color: #000000; + border-color: #CD7F32; + box-shadow: 0 4rpx 16rpx rgba(205, 127, 50, 0.4); +} + +.rank-other { + background: #2A2A2A; + color: #B0B0B0; + border-color: #3A3A3A; +} + +/* ==================== r ==================== */ + +.divider { + height: 1rpx; + background: #2A2A2A; + margin: 24rpx 0; +} + +.divider-gold { + height: 2rpx; + background: linear-gradient(90deg, transparent 0%, #D4AF37 20%, #D4AF37 80%, transparent 100%); + margin: 32rpx 0; +} + +/* ==================== (w{ ==================== */ + +.text-center { text-align: center; } +.text-left { text-align: left; } +.text-right { text-align: right; } + +.mt-xs { margin-top: 8rpx; } +.mt-sm { margin-top: 12rpx; } +.mt-md { margin-top: 16rpx; } +.mt-lg { margin-top: 24rpx; } +.mt-xl { margin-top: 32rpx; } + +.mb-xs { margin-bottom: 8rpx; } +.mb-sm { margin-bottom: 12rpx; } +.mb-md { margin-bottom: 16rpx; } +.mb-lg { margin-bottom: 24rpx; } +.mb-xl { margin-bottom: 32rpx; } + +.p-xs { padding: 8rpx; } +.p-sm { padding: 12rpx; } +.p-md { padding: 16rpx; } +.p-lg { padding: 24rpx; } +.p-xl { padding: 32rpx; } diff --git a/components/custom-tabbar/custom-tabbar.js b/components/custom-tabbar/custom-tabbar.js new file mode 100644 index 0000000..dd237df --- /dev/null +++ b/components/custom-tabbar/custom-tabbar.js @@ -0,0 +1,59 @@ +// components/custom-tabbar/custom-tabbar.js +Component({ + data: { + selected: 0, + color: "#666666", + selectedColor: "#667eea", + list: [ + { + pagePath: "/pages/index/index", + text: "打牌记账", + iconPath: "../images/index.png", + selectedIconPath: "../images/index_active.png" + }, + { + pagePath: "/pages/stats/personal/personal", + text: "统计分析", + iconPath: "../images/record.png", + selectedIconPath: "../images/record-active.png" + }, + { + pagePath: "/pages/profile/index/index", + text: "我的", + iconPath: "../images/my.png", + selectedIconPath: "../images/my-active.png" + } + ] + }, + + properties: { + selected: { + type: Number, + value: 0 + } + }, + + lifetimes: { + attached() { + // 组件初始化时更新选中状态 + this.setData({ + selected: this.data.selected + }) + } + }, + + methods: { + switchTab(e) { + const index = e.currentTarget.dataset.index + const pagePath = this.data.list[index].pagePath + + // 触发事件通知父组件 + this.triggerEvent('tabchange', { index, pagePath }) + + // 切换页面 + wx.switchTab({ + url: pagePath + }) + } + } +}) diff --git a/components/custom-tabbar/custom-tabbar.json b/components/custom-tabbar/custom-tabbar.json new file mode 100644 index 0000000..a89ef4d --- /dev/null +++ b/components/custom-tabbar/custom-tabbar.json @@ -0,0 +1,4 @@ +{ + "component": true, + "usingComponents": {} +} diff --git a/components/custom-tabbar/custom-tabbar.wxml b/components/custom-tabbar/custom-tabbar.wxml new file mode 100644 index 0000000..ff5bdcc --- /dev/null +++ b/components/custom-tabbar/custom-tabbar.wxml @@ -0,0 +1,23 @@ + + + + + + + + {{item.text}} + + + diff --git a/components/custom-tabbar/custom-tabbar.wxss b/components/custom-tabbar/custom-tabbar.wxss new file mode 100644 index 0000000..2253070 --- /dev/null +++ b/components/custom-tabbar/custom-tabbar.wxss @@ -0,0 +1,53 @@ +/* components/custom-tabbar/custom-tabbar.wxss */ +.tabbar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + height: 100rpx; + background: #1A1A1A; + border-top: 1rpx solid rgba(212, 175, 55, 0.2); + display: flex; + align-items: center; + justify-content: space-around; + box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.8); + z-index: 999; + padding-bottom: env(safe-area-inset-bottom); +} + +.tabbar-item { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 10rpx 0; + transition: all 0.3s ease; +} + +.tabbar-item.active { + transform: translateY(-4rpx); +} + +.tabbar-icon { + width: 48rpx; + height: 48rpx; + margin-bottom: 6rpx; +} + +.tabbar-icon image { + width: 100%; + height: 100%; +} + +.tabbar-text { + font-size: 22rpx; + color: #B0B0B0; + transition: all 0.3s ease; +} + +.tabbar-item.active .tabbar-text { + font-weight: 600; + font-size: 24rpx; + color: #D4AF37; +} diff --git a/components/guide/guide.js b/components/guide/guide.js new file mode 100644 index 0000000..675771b --- /dev/null +++ b/components/guide/guide.js @@ -0,0 +1,208 @@ +// 新用户引导组件 +Component({ + properties: { + show: { + type: Boolean, + value: false + }, + steps: { + type: Array, + value: [] + } + }, + + data: { + currentStep: 0, + totalSteps: 0, + title: '', + description: '', + spotlightStyle: {}, + contentStyle: '', + arrowDirection: 'top', + showArrow: true + }, + + observers: { + 'show': function(show) { + if (show && this.data.steps.length > 0) { + this.setData({ + currentStep: 0, + totalSteps: this.data.steps.length + }) + this.showStep(0) + } + } + }, + + methods: { + // 显示指定步骤 + showStep(stepIndex) { + if (stepIndex < 0 || stepIndex >= this.data.steps.length) { + return + } + + const step = this.data.steps[stepIndex] + this.setData({ + currentStep: stepIndex, + title: step.title, + description: step.description, + showArrow: step.showArrow !== false + }) + + // 延迟获取元素位置,确保页面已渲染 + setTimeout(() => { + this.updateSpotlight(step) + }, 50) + }, + + // 更新高亮区域 + updateSpotlight(step) { + const query = wx.createSelectorQuery().in(this) + + // 查询目标元素(在页面中) + const pageQuery = wx.createSelectorQuery() + pageQuery.select(step.selector).boundingClientRect() + pageQuery.selectViewport().scrollOffset() + + pageQuery.exec((res) => { + if (!res || !res[0]) { + console.warn('未找到目标元素:', step.selector) + return + } + + const rect = res[0] + const scroll = res[1] + + // 计算高亮区域样式(添加padding扩展区域) + const padding = step.padding || 8 + const spotlightStyle = { + top: rect.top - padding, + left: rect.left - padding, + width: rect.width + padding * 2, + height: rect.height + padding * 2, + borderRadius: step.borderRadius || 16 + } + + // 计算提示内容位置 + const contentStyle = this.calculateContentPosition(spotlightStyle, step.position || 'bottom') + + this.setData({ + spotlightStyle, + contentStyle: contentStyle.style, + arrowDirection: contentStyle.arrowDirection + }) + }) + }, + + // 计算提示内容位置 + calculateContentPosition(spotlightStyle, preferredPosition) { + const windowInfo = wx.getWindowInfo() + const screenHeight = windowInfo.windowHeight + const screenWidth = windowInfo.windowWidth + const contentWidth = 600 // rpx转换为px约300 + const contentMaxHeight = 400 // 估算最大高度 + + let position = preferredPosition + let top = 0 + let left = 0 + let arrowDirection = 'top' + + // 计算中心点 + const spotlightCenterX = spotlightStyle.left + spotlightStyle.width / 2 + const spotlightCenterY = spotlightStyle.top + spotlightStyle.height / 2 + + // 根据位置偏好计算 + switch (position) { + case 'top': + top = spotlightStyle.top - contentMaxHeight - 40 + left = Math.max(20, Math.min(spotlightCenterX - contentWidth / 2, screenWidth - contentWidth - 20)) + arrowDirection = 'bottom' + + // 如果顶部空间不足,改为底部 + if (top < 20) { + position = 'bottom' + } + break + + case 'bottom': + top = spotlightStyle.top + spotlightStyle.height + 20 + left = Math.max(20, Math.min(spotlightCenterX - contentWidth / 2, screenWidth - contentWidth - 20)) + arrowDirection = 'top' + + // 如果底部空间不足,改为顶部 + if (top + contentMaxHeight > screenHeight - 20) { + position = 'top' + top = spotlightStyle.top - contentMaxHeight - 40 + arrowDirection = 'bottom' + } + break + + case 'left': + left = spotlightStyle.left - contentWidth - 20 + top = Math.max(20, Math.min(spotlightCenterY - 100, screenHeight - contentMaxHeight - 20)) + arrowDirection = 'right' + + // 如果左侧空间不足,改为右侧 + if (left < 20) { + position = 'right' + } + break + + case 'right': + left = spotlightStyle.left + spotlightStyle.width + 20 + top = Math.max(20, Math.min(spotlightCenterY - 100, screenHeight - contentMaxHeight - 20)) + arrowDirection = 'left' + + // 如果右侧空间不足,改为左侧 + if (left + contentWidth > screenWidth - 20) { + position = 'left' + left = spotlightStyle.left - contentWidth - 20 + arrowDirection = 'right' + } + break + } + + // 如果重新计算了位置,递归调用 + if (position !== preferredPosition) { + return this.calculateContentPosition(spotlightStyle, position) + } + + return { + style: `top: ${top}px; left: ${left}px;`, + arrowDirection + } + }, + + // 下一步 + handleNext() { + const nextStep = this.data.currentStep + 1 + + if (nextStep >= this.data.steps.length) { + // 引导完成 + this.handleComplete() + } else { + // 显示下一步 + this.showStep(nextStep) + } + }, + + // 跳过引导 + handleSkip() { + this.handleComplete() + }, + + // 完成引导 + handleComplete() { + this.triggerEvent('complete') + }, + + // 点击遮罩(不做任何操作,防止穿透) + handleMaskTap() { + // 可选:点击遮罩也进入下一步 + // this.handleNext() + }, + + // 阻止滚动穿透 + preventMove() {} + } +}) diff --git a/components/guide/guide.json b/components/guide/guide.json new file mode 100644 index 0000000..a89ef4d --- /dev/null +++ b/components/guide/guide.json @@ -0,0 +1,4 @@ +{ + "component": true, + "usingComponents": {} +} diff --git a/components/guide/guide.wxml b/components/guide/guide.wxml new file mode 100644 index 0000000..618fa09 --- /dev/null +++ b/components/guide/guide.wxml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + {{title}} + {{description}} + + + + + + + + + + 跳过 + + {{currentStep === totalSteps - 1 ? '知道了' : '下一步'}} + + + + + + diff --git a/components/guide/guide.wxss b/components/guide/guide.wxss new file mode 100644 index 0000000..813a4d4 --- /dev/null +++ b/components/guide/guide.wxss @@ -0,0 +1,164 @@ +/* 新用户引导样式 */ + +/* 遮罩层 */ +.guide-mask { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 9999; +} + +.guide-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.75); +} + +/* 高亮区域(通过透明背景实现打洞效果)*/ +.guide-spotlight { + position: absolute; + background: transparent; + box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.75); + z-index: 10000; + transition: all 0.3s ease; +} + +/* 提示内容容器 */ +.guide-content { + position: absolute; + z-index: 10001; + max-width: 600rpx; + transition: all 0.3s ease; +} + +.guide-text-wrapper { + background: linear-gradient(135deg, #1A1A1A 0%, #252525 100%); + border: 1rpx solid rgba(212, 175, 55, 0.3); + border-radius: 16rpx; + padding: 32rpx 28rpx 24rpx; + box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.8); +} + +/* 箭头指示器 */ +.guide-arrow { + position: absolute; + width: 0; + height: 0; + border: 20rpx solid transparent; +} + +.guide-arrow.top { + bottom: 100%; + left: 50%; + margin-left: -20rpx; + border-bottom-color: #1A1A1A; + border-top: none; +} + +.guide-arrow.bottom { + top: 100%; + left: 50%; + margin-left: -20rpx; + border-top-color: #1A1A1A; + border-bottom: none; +} + +.guide-arrow.left { + right: 100%; + top: 50%; + margin-top: -20rpx; + border-right-color: #1A1A1A; + border-left: none; +} + +.guide-arrow.right { + left: 100%; + top: 50%; + margin-top: -20rpx; + border-left-color: #1A1A1A; + border-right: none; +} + +/* 文本样式 */ +.guide-title { + font-size: 32rpx; + font-weight: bold; + color: #F4E6C3; + margin-bottom: 16rpx; + line-height: 1.4; +} + +.guide-desc { + font-size: 26rpx; + color: #E0E0E0; + line-height: 1.6; + margin-bottom: 24rpx; +} + +/* 底部操作栏 */ +.guide-footer { + display: flex; + align-items: center; + justify-content: space-between; + padding-top: 20rpx; + border-top: 1rpx solid rgba(212, 175, 55, 0.2); +} + +/* 进度点 */ +.guide-dots { + display: flex; + gap: 12rpx; + align-items: center; +} + +.guide-dot { + width: 12rpx; + height: 12rpx; + border-radius: 50%; + background: #3A3A3A; + transition: all 0.3s ease; +} + +.guide-dot.active { + width: 32rpx; + border-radius: 6rpx; + background: linear-gradient(135deg, #D4AF37 0%, #F4E6C3 100%); + box-shadow: 0 2rpx 8rpx rgba(212, 175, 55, 0.4); +} + +/* 操作按钮 */ +.guide-actions { + display: flex; + gap: 24rpx; + align-items: center; +} + +.guide-skip { + font-size: 26rpx; + color: #B0B0B0; + padding: 8rpx 16rpx; +} + +.guide-next { + font-size: 26rpx; + font-weight: 500; + color: #000000; + background: linear-gradient(135deg, #D4AF37 0%, #F4E6C3 100%); + padding: 12rpx 32rpx; + border-radius: 24rpx; + box-shadow: 0 4rpx 12rpx rgba(212, 175, 55, 0.4); +} + +.guide-skip:active { + opacity: 0.6; +} + +.guide-next:active { + opacity: 0.9; + transform: scale(0.98); +} diff --git a/components/images/index.png b/components/images/index.png new file mode 100644 index 0000000..608c023 Binary files /dev/null and b/components/images/index.png differ diff --git a/components/images/index_active.png b/components/images/index_active.png new file mode 100644 index 0000000..34cced7 Binary files /dev/null and b/components/images/index_active.png differ diff --git a/components/images/my-active.png b/components/images/my-active.png new file mode 100644 index 0000000..b695521 Binary files /dev/null and b/components/images/my-active.png differ diff --git a/components/images/my.png b/components/images/my.png new file mode 100644 index 0000000..0d1c802 Binary files /dev/null and b/components/images/my.png differ diff --git a/components/images/record-active.png b/components/images/record-active.png new file mode 100644 index 0000000..2abcdae Binary files /dev/null and b/components/images/record-active.png differ diff --git a/components/images/record.png b/components/images/record.png new file mode 100644 index 0000000..0f87f1c Binary files /dev/null and b/components/images/record.png differ diff --git a/components/navbar/navbar.js b/components/navbar/navbar.js new file mode 100644 index 0000000..54f7e13 --- /dev/null +++ b/components/navbar/navbar.js @@ -0,0 +1,75 @@ +// components/navbar/navbar.js +Component({ + properties: { + // 标题 + title: { + type: String, + value: '' + }, + // 是否显示返回按钮 + showBack: { + type: Boolean, + value: true + }, + // 返回按钮文字 + backText: { + type: String, + value: '返回' + }, + // 背景色(支持渐变) + background: { + type: String, + value: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' + } + }, + + data: { + statusBarHeight: 0, + menuButtonTop: 0, + menuButtonHeight: 0, + navbarHeight: 0 + }, + + lifetimes: { + attached() { + this.setNavBarInfo() + } + }, + + methods: { + // 设置导航栏信息 + setNavBarInfo() { + const windowInfo = wx.getWindowInfo() + const menuButtonInfo = wx.getMenuButtonBoundingClientRect() + + const statusBarHeight = windowInfo.statusBarHeight + const menuButtonTop = menuButtonInfo.top + const menuButtonHeight = menuButtonInfo.height + + // 导航栏总高度 = 胶囊按钮bottom + 胶囊按钮到顶部的距离 + const navbarHeight = menuButtonInfo.bottom + (menuButtonTop - statusBarHeight) + + this.setData({ + statusBarHeight, + menuButtonTop, + menuButtonHeight, + navbarHeight + }) + }, + + // 返回按钮点击 + onBackTap() { + this.triggerEvent('back') + + // 如果没有监听back事件,默认返回上一页 + const pages = getCurrentPages() + if (pages.length > 1) { + wx.navigateBack() + } else { + wx.switchTab({ + url: '/pages/index/index' + }) + } + } + } +}) diff --git a/components/navbar/navbar.json b/components/navbar/navbar.json new file mode 100644 index 0000000..a89ef4d --- /dev/null +++ b/components/navbar/navbar.json @@ -0,0 +1,4 @@ +{ + "component": true, + "usingComponents": {} +} diff --git a/components/navbar/navbar.wxml b/components/navbar/navbar.wxml new file mode 100644 index 0000000..6b88844 --- /dev/null +++ b/components/navbar/navbar.wxml @@ -0,0 +1,17 @@ + + + + + + {{backText}} + + + + + {{title}} + + diff --git a/components/navbar/navbar.wxss b/components/navbar/navbar.wxss new file mode 100644 index 0000000..27b896e --- /dev/null +++ b/components/navbar/navbar.wxss @@ -0,0 +1,63 @@ +/* 自定义导航栏样式 */ +.custom-navbar { + position: fixed; + top: 0; + left: 0; + right: 0; + width: 100%; + z-index: 9999; + background: linear-gradient(135deg, #1A1A1A 0%, #252525 100%); + border-bottom: 1rpx solid rgba(212, 175, 55, 0.2); + box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.8); +} + +/* 返回按钮 */ +.nav-back-btn { + position: fixed; + left: 20rpx; + display: flex; + align-items: center; + justify-content: center; + gap: 8rpx; + padding: 0 24rpx; + background: rgba(212, 175, 55, 0.15); + border: 1rpx solid rgba(212, 175, 55, 0.3); + border-radius: 50rpx; + backdrop-filter: blur(10rpx); + z-index: 10000; +} + +.back-icon { + font-size: 32rpx; + color: white; + font-weight: bold; + line-height: 1; +} + +.back-text { + font-size: 28rpx; + color: white; + line-height: 1; +} + +/* 标题区域 */ +.nav-title-wrap { + position: fixed; + left: 0; + right: 0; + display: flex; + align-items: center; + justify-content: center; + z-index: 9998; +} + +.nav-title { + font-size: 34rpx; + font-weight: bold; + color: #F4E6C3; + max-width: 60%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + diff --git a/create-icons.js b/create-icons.js new file mode 100644 index 0000000..d4d43b6 --- /dev/null +++ b/create-icons.js @@ -0,0 +1,86 @@ +const fs = require('fs'); +const path = require('path'); + +// 创建SVG图标的函数 +function createSVGIcon(type, color) { + let svgContent = ''; + + switch(type) { + case 'home': + svgContent = ` + + +`; + break; + + case 'stats': + svgContent = ` + + + + +`; + break; + + case 'profile': + svgContent = ` + + + +`; + break; + } + + return svgContent; +} + +// 创建简单的PNG占位文件(使用最小的PNG数据) +function createMinimalPNG() { + // 最小的1x1 PNG图片的Base64数据 + const base64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='; + return Buffer.from(base64, 'base64'); +} + +// 图标配置 +const icons = [ + { name: 'home.png', type: 'home', color: '#999999' }, + { name: 'home-active.png', type: 'home', color: '#07c160' }, + { name: 'stats.png', type: 'stats', color: '#999999' }, + { name: 'stats-active.png', type: 'stats', color: '#07c160' }, + { name: 'profile.png', type: 'profile', color: '#999999' }, + { name: 'profile-active.png', type: 'profile', color: '#07c160' }, + { name: 'default-avatar.png', type: 'profile', color: '#cccccc' }, + { name: 'empty.png', type: 'home', color: '#dddddd' } +]; + +// 创建images目录(如果不存在) +const imagesDir = path.join(__dirname, 'images'); +if (!fs.existsSync(imagesDir)) { + fs.mkdirSync(imagesDir, { recursive: true }); +} + +// 创建所有图标文件(PNG占位符) +icons.forEach(icon => { + const filePath = path.join(imagesDir, icon.name); + const pngData = createMinimalPNG(); + fs.writeFileSync(filePath, pngData); + console.log(`创建图标: ${icon.name}`); +}); + +// 同时创建SVG版本供参考 +icons.forEach(icon => { + const svgFilePath = path.join(imagesDir, icon.name.replace('.png', '.svg')); + const svgContent = createSVGIcon(icon.type, icon.color); + if (svgContent) { + fs.writeFileSync(svgFilePath, svgContent); + console.log(`创建SVG: ${icon.name.replace('.png', '.svg')}`); + } +}); + +console.log('\n图标创建完成!'); +console.log('提示:'); +console.log('1. PNG文件是占位符,可以在微信开发者工具中正常运行'); +console.log('2. SVG文件显示了实际图标样式'); +console.log('3. 您可以使用在线工具将SVG转换为PNG'); +console.log('4. 或者打开generate-icons.html在浏览器中生成真实PNG图标'); \ No newline at end of file diff --git a/ext.json b/ext.json new file mode 100644 index 0000000..a99af51 --- /dev/null +++ b/ext.json @@ -0,0 +1,7 @@ +{ + "extEnable": true, + "extAppid": "wx171c02d83197be01", + "ext":{ + "appId":435 + } +} \ No newline at end of file diff --git a/generate-icons.html b/generate-icons.html new file mode 100644 index 0000000..93fc805 --- /dev/null +++ b/generate-icons.html @@ -0,0 +1,197 @@ + + + + + 生成TabBar图标 + + + +
+

打牌记账小程序 - TabBar图标生成器

+ +
+

使用说明:

+
    +
  1. 点击每个图标下方的"下载"按钮保存单个图标
  2. +
  3. 或点击"下载所有图标"按钮一次性下载全部
  4. +
  5. 将下载的图片保存到 miniapp/images/ 目录
  6. +
  7. 图标尺寸:81x81像素,适合小程序使用
  8. +
+
+ +
+ + +
+ + + + \ No newline at end of file diff --git a/images/default-avatar.png b/images/default-avatar.png new file mode 100644 index 0000000..4eb5879 Binary files /dev/null and b/images/default-avatar.png differ diff --git a/images/default-avatar.svg b/images/default-avatar.svg new file mode 100644 index 0000000..a3294ad --- /dev/null +++ b/images/default-avatar.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/images/empty.png b/images/empty.png new file mode 100644 index 0000000..613754c Binary files /dev/null and b/images/empty.png differ diff --git a/images/empty.svg b/images/empty.svg new file mode 100644 index 0000000..f48d285 --- /dev/null +++ b/images/empty.svg @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/images/home-active.png b/images/home-active.png new file mode 100644 index 0000000..613754c Binary files /dev/null and b/images/home-active.png differ diff --git a/images/home-active.svg b/images/home-active.svg new file mode 100644 index 0000000..fed54ec --- /dev/null +++ b/images/home-active.svg @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/images/home.png b/images/home.png new file mode 100644 index 0000000..613754c Binary files /dev/null and b/images/home.png differ diff --git a/images/home.svg b/images/home.svg new file mode 100644 index 0000000..9d5c262 --- /dev/null +++ b/images/home.svg @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/images/logo.svg b/images/logo.svg new file mode 100644 index 0000000..5077cd8 --- /dev/null +++ b/images/logo.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/profile-active.png b/images/profile-active.png new file mode 100644 index 0000000..613754c Binary files /dev/null and b/images/profile-active.png differ diff --git a/images/profile-active.svg b/images/profile-active.svg new file mode 100644 index 0000000..13becff --- /dev/null +++ b/images/profile-active.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/images/profile.png b/images/profile.png new file mode 100644 index 0000000..613754c Binary files /dev/null and b/images/profile.png differ diff --git a/images/profile.svg b/images/profile.svg new file mode 100644 index 0000000..8d507a3 --- /dev/null +++ b/images/profile.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/images/stats-active.png b/images/stats-active.png new file mode 100644 index 0000000..613754c Binary files /dev/null and b/images/stats-active.png differ diff --git a/images/stats-active.svg b/images/stats-active.svg new file mode 100644 index 0000000..f6bbc8e --- /dev/null +++ b/images/stats-active.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/images/stats.png b/images/stats.png new file mode 100644 index 0000000..613754c Binary files /dev/null and b/images/stats.png differ diff --git a/images/stats.svg b/images/stats.svg new file mode 100644 index 0000000..6135f2c --- /dev/null +++ b/images/stats.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/miniprogram_npm/@jdmini/api/index.d.ts b/miniprogram_npm/@jdmini/api/index.d.ts new file mode 100644 index 0000000..e06e58d --- /dev/null +++ b/miniprogram_npm/@jdmini/api/index.d.ts @@ -0,0 +1,295 @@ +// Generated by dts-bundle v0.7.3 + +declare module '@jdmini/api' { + import { onLoginReady, waitLogin } from '@jdmini/api/app'; + import HttpClient, { gatewayHttpClient, baseHttpClient, apiHttpClient } from '@jdmini/api/httpClient'; + import { injectApp, injectPage, injectComponent, hijackApp, hijackAllPage } from '@jdmini/api/injector'; + import adManager from '@jdmini/api/adManager'; + export { onLoginReady, waitLogin, injectApp, injectPage, injectComponent, hijackApp, hijackAllPage, gatewayHttpClient, baseHttpClient, apiHttpClient, HttpClient, adManager, }; +} + +declare module '@jdmini/api/app' { + export interface AppOptions { + gatewayUrl?: string; + baseUrl?: string; + apiUrl?: string; + } + export interface PageOptions { + showInterstitialAd?: boolean; + } + export function initApp(options?: AppOptions): Promise; + export function showPage(options: PageOptions | undefined, pageId: string): Promise; + export const checkTokenValid: () => boolean; + /** + * 确保登录完成 + * @param {Function} callback - 回调函数 + * @returns {void} + */ + export function onLoginReady(callback: (...args: any[]) => void): void; + /** + * 等待登录完成 + * @returns {Promise} + */ + export function waitLogin(): Promise; + export function login(): Promise; + export function fetchEchoData(): Promise; + export function trackVisit(): Promise; +} + +declare module '@jdmini/api/httpClient' { + import { HttpClientOptions, RequestOptions, ApiResponse } from '@jdmini/api/types'; + class HttpClient { + constructor({ baseURL, timeout }: HttpClientOptions); + setBaseURL(baseURL: string): void; + /** + * 请求 + * @param {string} path 路径 + * @param {string} method 方法, 默认GET + * @param {Object} data 数据, 默认{} + * @param {Object} options 传入wx.request的其他配置, 默认{} + * @returns {Promise} 返回一个Promise对象 + */ + request(path: string, method?: WechatMiniprogram.RequestOption['method'], data?: Record, options?: RequestOptions): Promise>; + /** + * 上传文件 + * @param {string} filePath 文件路径 + * @param {Object} data 数据, 默认{} + * @param {'avatar' | 'file'} type 类型, 默认'file' + * @returns {Promise} 返回一个Promise对象 + */ + uploadFile(filePath: string, data?: Record, type?: 'avatar' | 'file'): Promise>; + /** + * 上传文件 + * @param {string} filePath 文件路径 + * @param {Object} data 数据, 默认{} + * @param {'avatar' | 'file'} type 类型, 默认'file' + * @returns {Promise} 返回一个Promise对象 + */ + upload(path: string, filePath: string, data?: Record): Promise>; + /** + * 删除文件 + * @param {number} fileId 文件id + * @returns {Promise} 返回一个Promise对象 + */ + deleteFile(fileId: number): Promise>; + /** + * 上传头像 + * @param {string} filePath 文件路径 + * @returns {Promise} 返回一个Promise对象 + */ + uploadAvatar(filePath: string): Promise>; + } + export const gatewayHttpClient: HttpClient; + export const baseHttpClient: HttpClient; + export const apiHttpClient: HttpClient; + export default HttpClient; +} + +declare module '@jdmini/api/injector' { + interface AppConfig { + onLaunch?: (...args: any[]) => void | Promise; + [key: string]: any; + } + interface PageConfig { + onShow?: (...args: any[]) => void | Promise; + [key: string]: any; + } + interface ComponentConfig { + methods?: { + onLoad?: (...args: any[]) => void | Promise; + onShow?: (...args: any[]) => void | Promise; + [key: string]: any; + }; + [key: string]: any; + } + interface InjectAppOptions { + gatewayUrl?: string; + baseUrl?: string; + apiUrl?: string; + } + interface InjectPageOptions { + showInterstitialAd?: boolean; + } + /** + * 注入应用配置 + * @param {Object} options - 配置选项 + * @param {string} [options.gatewayUrl] - 网关地址,默认使用CONFIG.API.GATEWAY_URL + * @param {string} [options.baseUrl] - 基础地址,默认使用CONFIG.API.BASE_URL + * @param {string} [options.apiUrl] - api地址,默认使用CONFIG.API.API_URL + * @returns {Function} 返回一个接收应用配置的函数 + */ + export function injectApp(options?: InjectAppOptions): (appConfig: AppConfig) => AppConfig; + /** + * 注入页面配置 + * @param {InjectPageOptions} options - 配置选项 + * @param {boolean} [options.showInterstitialAd] - 是否在onShow显示插屏广告,默认不显示 + * @returns {Function} 返回一个接收页面配置的函数 + */ + export function injectPage(options?: InjectPageOptions): (pageConfig?: PageConfig) => PageConfig; + /** + * 注入组件配置 + * @param {InjectPageOptions} options - 配置选项 + * @param {boolean} [options.showInterstitialAd] - 是否在onShow显示插屏广告,默认不显示 + * @returns {Function} 返回一个接收组件配置的函数 + */ + export function injectComponent(options?: InjectPageOptions): (pageConfig?: PageConfig) => ComponentConfig; + /** + * 劫持App + * @param {InjectAppOptions} options - 配置选项 + * @param {string} [options.gatewayUrl] - 网关地址,默认使用CONFIG.API.GATEWAY_URL + * @param {string} [options.baseUrl] - 基础地址,默认使用CONFIG.API.BASE_URL + * @param {string} [options.apiUrl] - api地址,默认使用CONFIG.API.API_URL + * @returns {void} + */ + export const hijackApp: (options?: InjectAppOptions) => void; + /** + * 劫持所有Page + * @param {InjectPageOptions} options - 配置选项 + * @param {boolean} [options.showInterstitialAd] - 是否在onShow显示插屏广告,默认不显示 + * @returns {void} + */ + export const hijackAllPage: (options?: InjectPageOptions) => void; + export {}; +} + +declare module '@jdmini/api/adManager' { + import { AdData, LinkData, TopData } from '@jdmini/api/types'; + type Ads = Partial>; + class AdManager { + /** + * 广告数据 + */ + ads: Ads; + /** + * 友情链接数据 + */ + link: LinkData[]; + /** + * 友情链接顶部广告数据 + */ + top: TopData | null; + constructor(); + /** + * 确保广告数据就绪 + * @param {Function} callback - 回调函数 + * @returns {void} + */ + onDataReady: (callback: (...args: any[]) => void) => void; + /** + * 等待广告数据加载完成 + * @returns {Promise} + */ + waitAdData: () => Promise; + /** + * 初始化广告数据 + * @returns {void} + */ + init: () => void; + /** + * 创建并展示插屏广告 + * @returns {Promise} + */ + createAndShowInterstitialAd: () => Promise; + /** + * 创建并展示激励视频广告 + * @param {any} context - 页面上下文 + * @param {string} [pageId] - 页面ID + * @returns {Promise} 是否完成播放 + */ + createAndShowRewardedVideoAd: (context: any, pageId?: string) => Promise; + } + const _default: AdManager; + export default _default; +} + +declare module '@jdmini/api/types' { + export interface Config { + API: { + GATEWAY_URL: string; + BASE_URL: string; + API_URL: string; + }; + APP: { + APP_ID: number; + LOGIN_MAX_RETRY: number; + }; + HTTP: { + TIMEOUT: number; + }; + DATA: { + PAGE_ID: string; + }; + STORAGE_KEYS: { + TOKEN: string; + USER_INFO: string; + SPA_DATA: string; + LINK_DATA: string; + TOP_DATA: string; + }; + EVENT_KEYS: { + LOGIN_SUCCESS: string; + AD_DATA_READY: string; + REWARDED_VIDEO_AD_CLOSE: string; + }; + } + export interface HttpClientOptions { + baseURL: string; + timeout?: number; + } + export interface RequestOptions { + headers?: Record; + [key: string]: any; + } + export interface LoginData { + appId: number; + code: string; + brand: string; + model: string; + platform: string; + } + export interface ApiResponse { + code: number; + message?: string; + data?: T; + } + export interface UserInfo { + token: string; + user: { + id: number; + name: string; + avatar: string; + openId: string; + }; + } + export interface EchoData { + isPublished: boolean; + spads: AdData[]; + links: LinkData[]; + top: TopData; + version: number; + } + export interface AdData { + id: number; + status: number; + appPage: 'banner' | 'custom' | 'video' | 'interstitial' | 'rewarded'; + ads: { + id: number; + type: number; + adId: number; + adUnitId: string; + }[]; + } + export interface LinkData { + appId: string; + appLogo: string; + linkName: string; + linkPage: string; + } + export interface TopData { + appId: string; + appLogo: string; + linkName: string; + appDsc: string; + } +} + diff --git a/miniprogram_npm/@jdmini/api/index.js b/miniprogram_npm/@jdmini/api/index.js new file mode 100644 index 0000000..fc11843 --- /dev/null +++ b/miniprogram_npm/@jdmini/api/index.js @@ -0,0 +1,4 @@ +/*! + * @jdmini/api v1.0.10 + * + */(()=>{"use strict";var t={616:function(t,e,n){var r=this&&this.__awaiter||function(t,e,n,r){return new(n||(n=Promise))((function(o,a){function i(t){try{u(r.next(t))}catch(t){a(t)}}function s(t){try{u(r.throw(t))}catch(t){a(t)}}function u(t){var e;t.done?o(t.value):(e=t.value,e instanceof n?e:new n((function(t){t(e)}))).then(i,s)}u((r=r.apply(t,e||[])).next())}))},o=this&&this.__generator||function(t,e){var n,r,o,a={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]},i=Object.create(("function"==typeof Iterator?Iterator:Object).prototype);return i.next=s(0),i.throw=s(1),i.return=s(2),"function"==typeof Symbol&&(i[Symbol.iterator]=function(){return this}),i;function s(s){return function(u){return function(s){if(n)throw new TypeError("Generator is already executing.");for(;i&&(i=0,s[0]&&(a=0)),a;)try{if(n=1,r&&(o=2&s[0]?r.return:s[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,s[1])).done)return o;switch(r=0,o&&(s=[2&s[0],o.value]),s[0]){case 0:case 1:o=s;break;case 4:return a.label++,{value:s[1],done:!1};case 5:a.label++,r=s[1],s=[0];continue;case 7:s=a.ops.pop(),a.trys.pop();continue;default:if(!(o=a.trys,(o=o.length>0&&o[o.length-1])||6!==s[0]&&2!==s[0])){a=0;continue}if(3===s[0]&&(!o||s[1]>o[0]&&s[1]0?e.ads[0].adUnitId:"",t}),{}));var n=wx.getStorageSync(i.default.STORAGE_KEYS.LINK_DATA);n&&(t.link=n);var r=wx.getStorageSync(i.default.STORAGE_KEYS.TOP_DATA);r&&r.appDsc.length>40&&(r.appDsc=r.appDsc.substring(0,40)+"...",t.top=r),t.adDataReady=!0,s.default.emit(i.default.EVENT_KEYS.AD_DATA_READY)},this.createAndShowInterstitialAd=function(){return r(t,void 0,void 0,(function(){var t;return o(this,(function(e){switch(e.label){case 0:return this.interstitialAd&&this.interstitialAd.destroy(),[4,this.waitAdData()];case 1:e.sent(),e.label=2;case 2:return e.trys.push([2,5,,6]),[4,this.createInterstitialAd()];case 3:return e.sent(),[4,this.showInterstitialAd()];case 4:return e.sent(),[3,6];case 5:return t=e.sent(),console.error("创建插屏广告失败:",t),[3,6];case 6:return[2]}}))}))},this.createInterstitialAd=function(){return r(t,void 0,void 0,(function(){var t=this;return o(this,(function(e){return[2,new Promise((function(e,n){t.ads.interstitial?(t.interstitialAd=wx.createInterstitialAd({adUnitId:t.ads.interstitial}),t.interstitialAd.onLoad((function(){console.log("插屏广告加载成功"),e()})),t.interstitialAd.onError((function(t){console.error(t),n(new Error("插屏广告加载失败"))})),t.interstitialAd.onClose((function(){console.log("插屏广告关闭")}))):n(new Error("插屏广告未配置"))}))]}))}))},this.showInterstitialAd=function(){return r(t,void 0,void 0,(function(){var t,e;return o(this,(function(n){switch(n.label){case 0:return n.trys.push([0,2,,3]),console.log("开始展示插屏广告"),[4,null===(e=this.interstitialAd)||void 0===e?void 0:e.show()];case 1:return[2,n.sent()];case 2:return t=n.sent(),console.error("插屏广告展示失败:",t),[3,3];case 3:return[2]}}))}))},this.createAndShowRewardedVideoAd=function(e,n){return r(t,void 0,void 0,(function(){var t,r,a;return o(this,(function(o){switch(o.label){case 0:return[4,this.waitAdData()];case 1:o.sent(),t=n||(null===(a=null==e?void 0:e.data)||void 0===a?void 0:a[i.default.DATA.PAGE_ID]),o.label=2;case 2:if(o.trys.push([2,6,,7]),!t)throw new Error("未指定pageId或者context");return this.rewardedVideoAds[t]?[3,4]:[4,this.createRewardedVideoAd(t)];case 3:o.sent(),o.label=4;case 4:return[4,this.showRewardedVideoAd(t)];case 5:return o.sent(),[3,7];case 6:return r=o.sent(),console.error("创建激励视频广告失败:",r),[3,7];case 7:return[2,new Promise((function(e){s.default.on(i.default.EVENT_KEYS.REWARDED_VIDEO_AD_CLOSE,(function(n,r){n===t&&e(r)}))}))]}}))}))},this.createRewardedVideoAd=function(e){return r(t,void 0,void 0,(function(){var t=this;return o(this,(function(n){return[2,new Promise((function(n,r){t.ads.rewarded?(t.rewardedVideoAds[e]=wx.createRewardedVideoAd({adUnitId:t.ads.rewarded}),t.rewardedVideoAds[e].onLoad((function(){console.log("激励视频广告加载成功"),n()})),t.rewardedVideoAds[e].onError((function(t){console.error(t),r(new Error("激励视频广告加载失败"))})),t.rewardedVideoAds[e].onClose((function(t){s.default.emit(i.default.EVENT_KEYS.REWARDED_VIDEO_AD_CLOSE,e,t.isEnded),console.log("激励视频广告关闭")}))):r(new Error("激励视频广告未配置"))}))]}))}))},this.showRewardedVideoAd=function(e){return r(t,void 0,void 0,(function(){var t,n;return o(this,(function(r){switch(r.label){case 0:return r.trys.push([0,2,,3]),console.log("开始展示激励视频广告"),[4,null===(n=this.rewardedVideoAds[e])||void 0===n?void 0:n.show()];case 1:return[2,r.sent()];case 2:return t=r.sent(),console.error("激励视频广告展示失败:",t),[3,3];case 3:return[2]}}))}))}};e.default=new u},859:function(t,e,n){var r=this&&this.__awaiter||function(t,e,n,r){return new(n||(n=Promise))((function(o,a){function i(t){try{u(r.next(t))}catch(t){a(t)}}function s(t){try{u(r.throw(t))}catch(t){a(t)}}function u(t){var e;t.done?o(t.value):(e=t.value,e instanceof n?e:new n((function(t){t(e)}))).then(i,s)}u((r=r.apply(t,e||[])).next())}))},o=this&&this.__generator||function(t,e){var n,r,o,a={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]},i=Object.create(("function"==typeof Iterator?Iterator:Object).prototype);return i.next=s(0),i.throw=s(1),i.return=s(2),"function"==typeof Symbol&&(i[Symbol.iterator]=function(){return this}),i;function s(s){return function(u){return function(s){if(n)throw new TypeError("Generator is already executing.");for(;i&&(i=0,s[0]&&(a=0)),a;)try{if(n=1,r&&(o=2&s[0]?r.return:s[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,s[1])).done)return o;switch(r=0,o&&(s=[2&s[0],o.value]),s[0]){case 0:case 1:o=s;break;case 4:return a.label++,{value:s[1],done:!1};case 5:a.label++,r=s[1],s=[0];continue;case 7:s=a.ops.pop(),a.trys.pop();continue;default:if(!(o=a.trys,(o=o.length>0&&o[o.length-1])||6!==s[0]&&2!==s[0])){a=0;continue}if(3===s[0]&&(!o||s[1]>o[0]&&s[1]s.default.APP.LOGIN_MAX_RETRY)throw wx.showToast({title:"网络异常,无法初始化",icon:"none",duration:2e3}),new Error("网络异常,无法初始化");o.label=1;case 1:return o.trys.push([1,7,,9]),t=wx.getDeviceInfo(),[4,wx.login()];case 2:return e=o.sent().code,n={appId:s.default.APP.APP_ID,code:e,brand:t.brand,model:t.model,platform:t.platform},[4,i.gatewayHttpClient.request("/wx/v1/api/login","POST",n)];case 3:return 200===(r=o.sent()).code&&r.data?(wx.setStorageSync(s.default.STORAGE_KEYS.TOKEN,r.data.token),wx.setStorageSync(s.default.STORAGE_KEYS.USER_INFO,r.data.user),l=0,u.default.emit(s.default.EVENT_KEYS.LOGIN_SUCCESS),[3,6]):[3,4];case 4:return l++,[4,h()];case 5:o.sent(),o.label=6;case 6:return[3,9];case 7:return o.sent(),l++,[4,h()];case 8:return o.sent(),[3,9];case 9:return[2]}}))}))}function p(){return r(this,void 0,void 0,(function(){var t;return o(this,(function(n){switch(n.label){case 0:return wx.removeStorageSync(s.default.STORAGE_KEYS.SPA_DATA),wx.removeStorageSync(s.default.STORAGE_KEYS.LINK_DATA),wx.removeStorageSync(s.default.STORAGE_KEYS.TOP_DATA),(0,e.checkTokenValid)()?[4,i.gatewayHttpClient.request("/wx/v1/api/echo","GET")]:[2];case 1:return 200===(t=n.sent()).code&&t.data?(t.data.spads&&wx.setStorageSync(s.default.STORAGE_KEYS.SPA_DATA,t.data.spads),t.data.links&&wx.setStorageSync(s.default.STORAGE_KEYS.LINK_DATA,t.data.links),t.data.top&&wx.setStorageSync(s.default.STORAGE_KEYS.TOP_DATA,t.data.top),[3,5]):[3,2];case 2:return 401!==t.code?[3,5]:[4,h()];case 3:return n.sent(),[4,p()];case 4:n.sent(),n.label=5;case 5:return[2]}}))}))}function w(){return r(this,void 0,void 0,(function(){var t;return o(this,(function(e){switch(e.label){case 0:return e.trys.push([0,5,,6]),[4,i.gatewayHttpClient.request("/wx/v1/api/visit","POST")];case 1:return 401!==e.sent().code?[3,4]:[4,h()];case 2:return e.sent(),[4,w()];case 3:e.sent(),e.label=4;case 4:return[3,6];case 5:return t=e.sent(),console.error("访问统计失败:",t),[3,6];case 6:return[2]}}))}))}e.checkTokenValid=function(){var t=wx.getStorageSync(s.default.STORAGE_KEYS.TOKEN);return!(!t||t.length<32)}},28:(t,e)=>{Object.defineProperty(e,"__esModule",{value:!0});var n={API:{GATEWAY_URL:"https://ca.miniappapi.com",BASE_URL:"https://app.jd027.com/v1/api",API_URL:"https://cp.miniappapi.com"},APP:{APP_ID:wx.getExtConfigSync().appId||313,LOGIN_MAX_RETRY:2},HTTP:{TIMEOUT:5e3},DATA:{PAGE_ID:"jdwx-page-id"},STORAGE_KEYS:{TOKEN:"jdwx-token",USER_INFO:"jdwx-userinfo",SPA_DATA:"jdwx-spadata",LINK_DATA:"jdwx-linkdata",TOP_DATA:"jdwx-topdata"},EVENT_KEYS:{LOGIN_SUCCESS:"jdwx-login-success",AD_DATA_READY:"jdwx-ad-data-ready",REWARDED_VIDEO_AD_CLOSE:"jdwx-rewarded-video-ad-close"}};e.default=n},144:(t,e)=>{Object.defineProperty(e,"__esModule",{value:!0});var n=function(){function t(){this.events={}}return t.prototype.on=function(t,e){if("string"!=typeof t)throw new TypeError("eventName must be a string");if("function"!=typeof e)throw new TypeError("callback must be a function");return this.events[t]||(this.events[t]=[]),this.events[t].push(e),this},t.prototype.emit=function(t){for(var e=[],n=1;n0&&o[o.length-1])||6!==s[0]&&2!==s[0])){a=0;continue}if(3===s[0]&&(!o||s[1]>o[0]&&s[1]=200&&d.statusCode<300)return[2,d.data];if(401===d.statusCode)return[2,{code:401,message:"未授权"}];throw new Error(d.data.message||"请求失败");case 4:throw f=a.sent(),console.error("网络错误:",f),wx.showToast({title:f instanceof Error?f.message:"网络错误",icon:"none",duration:2e3}),f;case 5:return[2]}}))}))},t.prototype.uploadFile=function(t){return o(this,arguments,void 0,(function(t,e,n){var r,o,i,u;return void 0===e&&(e={}),void 0===n&&(n="file"),a(this,(function(a){r=this.baseURL===s.default.API.GATEWAY_URL,o=wx.getStorageSync(s.default.STORAGE_KEYS.TOKEN),i="avatar"===n?"/avatar":"/file/new",u=this.joinURL(this.baseURL,"".concat(r?"/wx/v1/api":"").concat(i));try{return[2,new Promise((function(n,r){wx.uploadFile({url:u,name:"file",filePath:t,formData:e,header:{"Content-Type":"application/x-www-form-urlencoded",Authorization:o},success:function(t){if(t.statusCode>=200&&t.statusCode<300)n(JSON.parse(t.data));else{if(401!==t.statusCode)throw new Error(t.data.message||"上传失败");n(JSON.parse(t.data))}},fail:function(){throw new Error("网络错误")}}).onProgressUpdate((function(t){console.log("上传进度",t.progress),console.log("已经上传的数据长度",t.totalBytesSent),console.log("预期需要上传的数据总长度",t.totalBytesExpectedToSend)}))}))]}catch(t){throw console.error("上传失败:",t),wx.showToast({title:t instanceof Error?t.message:"上传失败",icon:"none",duration:2e3}),t}return[2]}))}))},t.prototype.upload=function(t,e){return o(this,arguments,void 0,(function(t,e,n){var r,o,i;return void 0===n&&(n={}),a(this,(function(a){r=this.baseURL===s.default.API.GATEWAY_URL,o=wx.getStorageSync(s.default.STORAGE_KEYS.TOKEN),i=this.joinURL(this.baseURL,"".concat(r?"/wx/v1/api":"").concat(t));try{return[2,new Promise((function(t,r){wx.uploadFile({url:i,name:"file",filePath:e,formData:n,header:{"Content-Type":"application/x-www-form-urlencoded",Authorization:o},success:function(e){if(e.statusCode>=200&&e.statusCode<300)t(JSON.parse(e.data));else{if(401!==e.statusCode)throw new Error(e.data.message||"上传失败");t(JSON.parse(e.data))}},fail:function(){throw new Error("网络错误")}}).onProgressUpdate((function(t){console.log("上传进度",t.progress),console.log("已经上传的数据长度",t.totalBytesSent),console.log("预期需要上传的数据总长度",t.totalBytesExpectedToSend)}))}))]}catch(t){throw console.error("上传失败:",t),wx.showToast({title:t instanceof Error?t.message:"上传失败",icon:"none",duration:2e3}),t}return[2]}))}))},t.prototype.deleteFile=function(t){return o(this,void 0,void 0,(function(){var e;return a(this,(function(n){return e=this.baseURL===s.default.API.GATEWAY_URL,[2,this.request("".concat(e?"/wx/v1/api":"","/file/del"),"GET",{id:t})]}))}))},t.prototype.uploadAvatar=function(t){return o(this,void 0,void 0,(function(){return a(this,(function(e){return[2,this.uploadFile(t,{},"avatar")]}))}))},t}();e.gatewayHttpClient=new u({baseURL:s.default.API.GATEWAY_URL,timeout:s.default.HTTP.TIMEOUT}),e.baseHttpClient=new u({baseURL:s.default.API.BASE_URL,timeout:s.default.HTTP.TIMEOUT}),e.apiHttpClient=new u({baseURL:s.default.API.API_URL,timeout:s.default.HTTP.TIMEOUT}),e.default=u},156:function(t,e,n){var r=this&&this.__createBinding||(Object.create?function(t,e,n,r){void 0===r&&(r=n);var o=Object.getOwnPropertyDescriptor(e,n);o&&!("get"in o?!e.__esModule:o.writable||o.configurable)||(o={enumerable:!0,get:function(){return e[n]}}),Object.defineProperty(t,r,o)}:function(t,e,n,r){void 0===r&&(r=n),t[r]=e[n]}),o=this&&this.__setModuleDefault||(Object.create?function(t,e){Object.defineProperty(t,"default",{enumerable:!0,value:e})}:function(t,e){t.default=e}),a=this&&this.__importStar||function(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var n in t)"default"!==n&&Object.prototype.hasOwnProperty.call(t,n)&&r(e,t,n);return o(e,t),e},i=this&&this.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(e,"__esModule",{value:!0}),e.adManager=e.HttpClient=e.apiHttpClient=e.baseHttpClient=e.gatewayHttpClient=e.hijackAllPage=e.hijackApp=e.injectComponent=e.injectPage=e.injectApp=e.waitLogin=e.onLoginReady=void 0;var s=n(859);Object.defineProperty(e,"onLoginReady",{enumerable:!0,get:function(){return s.onLoginReady}}),Object.defineProperty(e,"waitLogin",{enumerable:!0,get:function(){return s.waitLogin}});var u=a(n(161));e.HttpClient=u.default,Object.defineProperty(e,"gatewayHttpClient",{enumerable:!0,get:function(){return u.gatewayHttpClient}}),Object.defineProperty(e,"baseHttpClient",{enumerable:!0,get:function(){return u.baseHttpClient}}),Object.defineProperty(e,"apiHttpClient",{enumerable:!0,get:function(){return u.apiHttpClient}});var c=n(718);Object.defineProperty(e,"injectApp",{enumerable:!0,get:function(){return c.injectApp}}),Object.defineProperty(e,"injectPage",{enumerable:!0,get:function(){return c.injectPage}}),Object.defineProperty(e,"injectComponent",{enumerable:!0,get:function(){return c.injectComponent}}),Object.defineProperty(e,"hijackApp",{enumerable:!0,get:function(){return c.hijackApp}}),Object.defineProperty(e,"hijackAllPage",{enumerable:!0,get:function(){return c.hijackAllPage}});var l=i(n(616));e.adManager=l.default},718:function(t,e,n){var r=this&&this.__assign||function(){return r=Object.assign||function(t){for(var e,n=1,r=arguments.length;n { + this.setData({ ads: adManager.ads }) + }) + }, + }, + methods: { + } +}) diff --git a/miniprogram_npm/@jdmini/components/jdwx-ad/index.json b/miniprogram_npm/@jdmini/components/jdwx-ad/index.json new file mode 100644 index 0000000..32640e0 --- /dev/null +++ b/miniprogram_npm/@jdmini/components/jdwx-ad/index.json @@ -0,0 +1,3 @@ +{ + "component": true +} \ No newline at end of file diff --git a/miniprogram_npm/@jdmini/components/jdwx-ad/index.wxml b/miniprogram_npm/@jdmini/components/jdwx-ad/index.wxml new file mode 100644 index 0000000..1b24adf --- /dev/null +++ b/miniprogram_npm/@jdmini/components/jdwx-ad/index.wxml @@ -0,0 +1,5 @@ + + + + + diff --git a/miniprogram_npm/@jdmini/components/jdwx-ad/index.wxss b/miniprogram_npm/@jdmini/components/jdwx-ad/index.wxss new file mode 100644 index 0000000..de627db --- /dev/null +++ b/miniprogram_npm/@jdmini/components/jdwx-ad/index.wxss @@ -0,0 +1,7 @@ +.jdwx-ad-component { + padding: 10rpx; +} + +.jdwx-ad-item { + bottom: 10rpx; +} \ No newline at end of file diff --git a/miniprogram_npm/@jdmini/components/jdwx-link/index.js b/miniprogram_npm/@jdmini/components/jdwx-link/index.js new file mode 100644 index 0000000..887d749 --- /dev/null +++ b/miniprogram_npm/@jdmini/components/jdwx-link/index.js @@ -0,0 +1,37 @@ +import { adManager } from '@jdmini/api' + +Component({ + properties: { + }, + data: { + link: [], + top: {}, + }, + pageLifetimes: { + show: function () { + adManager.onDataReady(() => { + this.setData({ + link: adManager.link, + top: adManager.top + }) + }) + }, + }, + methods: { + gotopLink: function () { + wx.vibrateShort() + wx.openEmbeddedMiniProgram({ + appId: this.data.top.appId, + path: this.data.top.linkPage + }); + }, + goLink: function (e) { + let index = e.currentTarget.id + wx.vibrateShort() + wx.openEmbeddedMiniProgram({ + appId: this.data.link[index].appId, + path: this.data.link[index].linkPage + }); + }, + } +}) diff --git a/miniprogram_npm/@jdmini/components/jdwx-link/index.json b/miniprogram_npm/@jdmini/components/jdwx-link/index.json new file mode 100644 index 0000000..fba482a --- /dev/null +++ b/miniprogram_npm/@jdmini/components/jdwx-link/index.json @@ -0,0 +1,3 @@ +{ + "component": true +} \ No newline at end of file diff --git a/miniprogram_npm/@jdmini/components/jdwx-link/index.wxml b/miniprogram_npm/@jdmini/components/jdwx-link/index.wxml new file mode 100644 index 0000000..30ff009 --- /dev/null +++ b/miniprogram_npm/@jdmini/components/jdwx-link/index.wxml @@ -0,0 +1,11 @@ + + + + {{top.linkName}} + {{top.appDsc}} + + + + {{item.linkName}} + + \ No newline at end of file diff --git a/miniprogram_npm/@jdmini/components/jdwx-link/index.wxss b/miniprogram_npm/@jdmini/components/jdwx-link/index.wxss new file mode 100644 index 0000000..111b9f1 --- /dev/null +++ b/miniprogram_npm/@jdmini/components/jdwx-link/index.wxss @@ -0,0 +1,63 @@ +/* 页面容器 */ +.jdwx-link-component { + /* background-image: linear-gradient(to right, #4F9863, #4F9863); */ + background-attachment: fixed; + background-size: cover; /* 确保背景图像覆盖整个元素 */ + /* height: 100vh; */ + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + padding: 20rpx; + box-sizing: border-box; + overflow: auto; /* 允许内容滚动 */ +} + +/* 列表项样式 */ +.jdwx-applink-list { + display: flex; + align-items: center; + width: 95%; + /* 假设我们想要每个view的高度约为屏幕高度的1/8 */ + /* 使用小程序的wx.getSystemInfo API动态计算并设置这个值会更准确 */ + height: calc((100vh - 40rpx) / 10); /* 减去容器padding的影响 */ + padding: 20rpx; + background-color: rgba(248, 250, 252, 1); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + border-radius: 8rpx; + margin-bottom: 10rpx; +} +.jdwx-applink-top { + display: flex; + align-items: center; + width: 95%; + /* 假设我们想要每个view的高度约为屏幕高度的1/8 */ + /* 使用小程序的wx.getSystemInfo API动态计算并设置这个值会更准确 */ + height: calc((100vh - 40rpx) / 6); /* 减去容器padding的影响 */ + padding: 20rpx; + border-radius: 8rpx; + margin-bottom: 30rpx; + background-color: rgba(248, 250, 252, 1); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} +.jdwx-applink-top-linkname{ + display: flex; + font-size: 36rpx; + color: rgb(39, 37, 37); + padding-bottom: 10rpx; +} +/* 图标样式 */ +.jdwx-applink-icon { + width: 120rpx; + height: 120rpx; + border-radius: 50%; + margin-right: 50rpx; + margin-left: 30rpx; +} + +/* 文本样式 */ +.jdwx-applink-text { + flex: 1; + font-size: 32rpx; + color: rgb(39, 37, 37); +} \ No newline at end of file diff --git a/miniprogram_npm/extend/index.js b/miniprogram_npm/extend/index.js new file mode 100644 index 0000000..1cb5e04 --- /dev/null +++ b/miniprogram_npm/extend/index.js @@ -0,0 +1,130 @@ +module.exports = (function() { +var __MODS__ = {}; +var __DEFINE__ = function(modId, func, req) { var m = { exports: {}, _tempexports: {} }; __MODS__[modId] = { status: 0, func: func, req: req, m: m }; }; +var __REQUIRE__ = function(modId, source) { if(!__MODS__[modId]) return require(source); if(!__MODS__[modId].status) { var m = __MODS__[modId].m; m._exports = m._tempexports; var desp = Object.getOwnPropertyDescriptor(m, "exports"); if (desp && desp.configurable) Object.defineProperty(m, "exports", { set: function (val) { if(typeof val === "object" && val !== m._exports) { m._exports.__proto__ = val.__proto__; Object.keys(val).forEach(function (k) { m._exports[k] = val[k]; }); } m._tempexports = val }, get: function () { return m._tempexports; } }); __MODS__[modId].status = 1; __MODS__[modId].func(__MODS__[modId].req, m, m.exports); } return __MODS__[modId].m.exports; }; +var __REQUIRE_WILDCARD__ = function(obj) { if(obj && obj.__esModule) { return obj; } else { var newObj = {}; if(obj != null) { for(var k in obj) { if (Object.prototype.hasOwnProperty.call(obj, k)) newObj[k] = obj[k]; } } newObj.default = obj; return newObj; } }; +var __REQUIRE_DEFAULT__ = function(obj) { return obj && obj.__esModule ? obj.default : obj; }; +__DEFINE__(1761629501967, function(require, module, exports) { + + +var hasOwn = Object.prototype.hasOwnProperty; +var toStr = Object.prototype.toString; +var defineProperty = Object.defineProperty; +var gOPD = Object.getOwnPropertyDescriptor; + +var isArray = function isArray(arr) { + if (typeof Array.isArray === 'function') { + return Array.isArray(arr); + } + + return toStr.call(arr) === '[object Array]'; +}; + +var isPlainObject = function isPlainObject(obj) { + if (!obj || toStr.call(obj) !== '[object Object]') { + return false; + } + + var hasOwnConstructor = hasOwn.call(obj, 'constructor'); + var hasIsPrototypeOf = obj.constructor && obj.constructor.prototype && hasOwn.call(obj.constructor.prototype, 'isPrototypeOf'); + // Not own constructor property must be Object + if (obj.constructor && !hasOwnConstructor && !hasIsPrototypeOf) { + return false; + } + + // Own properties are enumerated firstly, so to speed up, + // if last one is own, then all properties are own. + var key; + for (key in obj) { /**/ } + + return typeof key === 'undefined' || hasOwn.call(obj, key); +}; + +// If name is '__proto__', and Object.defineProperty is available, define __proto__ as an own property on target +var setProperty = function setProperty(target, options) { + if (defineProperty && options.name === '__proto__') { + defineProperty(target, options.name, { + enumerable: true, + configurable: true, + value: options.newValue, + writable: true + }); + } else { + target[options.name] = options.newValue; + } +}; + +// Return undefined instead of __proto__ if '__proto__' is not an own property +var getProperty = function getProperty(obj, name) { + if (name === '__proto__') { + if (!hasOwn.call(obj, name)) { + return void 0; + } else if (gOPD) { + // In early versions of node, obj['__proto__'] is buggy when obj has + // __proto__ as an own property. Object.getOwnPropertyDescriptor() works. + return gOPD(obj, name).value; + } + } + + return obj[name]; +}; + +module.exports = function extend() { + var options, name, src, copy, copyIsArray, clone; + var target = arguments[0]; + var i = 1; + var length = arguments.length; + var deep = false; + + // Handle a deep copy situation + if (typeof target === 'boolean') { + deep = target; + target = arguments[1] || {}; + // skip the boolean and the target + i = 2; + } + if (target == null || (typeof target !== 'object' && typeof target !== 'function')) { + target = {}; + } + + for (; i < length; ++i) { + options = arguments[i]; + // Only deal with non-null/undefined values + if (options != null) { + // Extend the base object + for (name in options) { + src = getProperty(target, name); + copy = getProperty(options, name); + + // Prevent never-ending loop + if (target !== copy) { + // Recurse if we're merging plain objects or arrays + if (deep && copy && (isPlainObject(copy) || (copyIsArray = isArray(copy)))) { + if (copyIsArray) { + copyIsArray = false; + clone = src && isArray(src) ? src : []; + } else { + clone = src && isPlainObject(src) ? src : {}; + } + + // Never move original objects, clone them + setProperty(target, { name: name, newValue: extend(deep, clone, copy) }); + + // Don't bring in undefined values + } else if (typeof copy !== 'undefined') { + setProperty(target, { name: name, newValue: copy }); + } + } + } + } + } + + // Return the modified object + return target; +}; + +}, function(modId) {var map = {}; return __REQUIRE__(map[modId], modId); }) +return __REQUIRE__(1761629501967); +})() +//miniprogram-npm-outsideDeps=[] +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/miniprogram_npm/extend/index.js.map b/miniprogram_npm/extend/index.js.map new file mode 100644 index 0000000..ce76fe9 --- /dev/null +++ b/miniprogram_npm/extend/index.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["index.js"],"names":[],"mappings":";;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"index.js","sourcesContent":["\n\nvar hasOwn = Object.prototype.hasOwnProperty;\nvar toStr = Object.prototype.toString;\nvar defineProperty = Object.defineProperty;\nvar gOPD = Object.getOwnPropertyDescriptor;\n\nvar isArray = function isArray(arr) {\n\tif (typeof Array.isArray === 'function') {\n\t\treturn Array.isArray(arr);\n\t}\n\n\treturn toStr.call(arr) === '[object Array]';\n};\n\nvar isPlainObject = function isPlainObject(obj) {\n\tif (!obj || toStr.call(obj) !== '[object Object]') {\n\t\treturn false;\n\t}\n\n\tvar hasOwnConstructor = hasOwn.call(obj, 'constructor');\n\tvar hasIsPrototypeOf = obj.constructor && obj.constructor.prototype && hasOwn.call(obj.constructor.prototype, 'isPrototypeOf');\n\t// Not own constructor property must be Object\n\tif (obj.constructor && !hasOwnConstructor && !hasIsPrototypeOf) {\n\t\treturn false;\n\t}\n\n\t// Own properties are enumerated firstly, so to speed up,\n\t// if last one is own, then all properties are own.\n\tvar key;\n\tfor (key in obj) { /**/ }\n\n\treturn typeof key === 'undefined' || hasOwn.call(obj, key);\n};\n\n// If name is '__proto__', and Object.defineProperty is available, define __proto__ as an own property on target\nvar setProperty = function setProperty(target, options) {\n\tif (defineProperty && options.name === '__proto__') {\n\t\tdefineProperty(target, options.name, {\n\t\t\tenumerable: true,\n\t\t\tconfigurable: true,\n\t\t\tvalue: options.newValue,\n\t\t\twritable: true\n\t\t});\n\t} else {\n\t\ttarget[options.name] = options.newValue;\n\t}\n};\n\n// Return undefined instead of __proto__ if '__proto__' is not an own property\nvar getProperty = function getProperty(obj, name) {\n\tif (name === '__proto__') {\n\t\tif (!hasOwn.call(obj, name)) {\n\t\t\treturn void 0;\n\t\t} else if (gOPD) {\n\t\t\t// In early versions of node, obj['__proto__'] is buggy when obj has\n\t\t\t// __proto__ as an own property. Object.getOwnPropertyDescriptor() works.\n\t\t\treturn gOPD(obj, name).value;\n\t\t}\n\t}\n\n\treturn obj[name];\n};\n\nmodule.exports = function extend() {\n\tvar options, name, src, copy, copyIsArray, clone;\n\tvar target = arguments[0];\n\tvar i = 1;\n\tvar length = arguments.length;\n\tvar deep = false;\n\n\t// Handle a deep copy situation\n\tif (typeof target === 'boolean') {\n\t\tdeep = target;\n\t\ttarget = arguments[1] || {};\n\t\t// skip the boolean and the target\n\t\ti = 2;\n\t}\n\tif (target == null || (typeof target !== 'object' && typeof target !== 'function')) {\n\t\ttarget = {};\n\t}\n\n\tfor (; i < length; ++i) {\n\t\toptions = arguments[i];\n\t\t// Only deal with non-null/undefined values\n\t\tif (options != null) {\n\t\t\t// Extend the base object\n\t\t\tfor (name in options) {\n\t\t\t\tsrc = getProperty(target, name);\n\t\t\t\tcopy = getProperty(options, name);\n\n\t\t\t\t// Prevent never-ending loop\n\t\t\t\tif (target !== copy) {\n\t\t\t\t\t// Recurse if we're merging plain objects or arrays\n\t\t\t\t\tif (deep && copy && (isPlainObject(copy) || (copyIsArray = isArray(copy)))) {\n\t\t\t\t\t\tif (copyIsArray) {\n\t\t\t\t\t\t\tcopyIsArray = false;\n\t\t\t\t\t\t\tclone = src && isArray(src) ? src : [];\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tclone = src && isPlainObject(src) ? src : {};\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Never move original objects, clone them\n\t\t\t\t\t\tsetProperty(target, { name: name, newValue: extend(deep, clone, copy) });\n\n\t\t\t\t\t// Don't bring in undefined values\n\t\t\t\t\t} else if (typeof copy !== 'undefined') {\n\t\t\t\t\t\tsetProperty(target, { name: name, newValue: copy });\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Return the modified object\n\treturn target;\n};\n"]} \ No newline at end of file diff --git a/miniprogram_npm/weapp-qrcode/index.js b/miniprogram_npm/weapp-qrcode/index.js new file mode 100644 index 0000000..fcadbf3 --- /dev/null +++ b/miniprogram_npm/weapp-qrcode/index.js @@ -0,0 +1,18 @@ +module.exports = (function() { +var __MODS__ = {}; +var __DEFINE__ = function(modId, func, req) { var m = { exports: {}, _tempexports: {} }; __MODS__[modId] = { status: 0, func: func, req: req, m: m }; }; +var __REQUIRE__ = function(modId, source) { if(!__MODS__[modId]) return require(source); if(!__MODS__[modId].status) { var m = __MODS__[modId].m; m._exports = m._tempexports; var desp = Object.getOwnPropertyDescriptor(m, "exports"); if (desp && desp.configurable) Object.defineProperty(m, "exports", { set: function (val) { if(typeof val === "object" && val !== m._exports) { m._exports.__proto__ = val.__proto__; Object.keys(val).forEach(function (k) { m._exports[k] = val[k]; }); } m._tempexports = val }, get: function () { return m._tempexports; } }); __MODS__[modId].status = 1; __MODS__[modId].func(__MODS__[modId].req, m, m.exports); } return __MODS__[modId].m.exports; }; +var __REQUIRE_WILDCARD__ = function(obj) { if(obj && obj.__esModule) { return obj; } else { var newObj = {}; if(obj != null) { for(var k in obj) { if (Object.prototype.hasOwnProperty.call(obj, k)) newObj[k] = obj[k]; } } newObj.default = obj; return newObj; } }; +var __REQUIRE_DEFAULT__ = function(obj) { return obj && obj.__esModule ? obj.default : obj; }; +__DEFINE__(1761629501968, function(require, module, exports) { +/** + * 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++}},module.exports=drawQrcode; + +}, function(modId) {var map = {}; return __REQUIRE__(map[modId], modId); }) +return __REQUIRE__(1761629501968); +})() +//miniprogram-npm-outsideDeps=[] +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/miniprogram_npm/weapp-qrcode/index.js.map b/miniprogram_npm/weapp-qrcode/index.js.map new file mode 100644 index 0000000..cfee516 --- /dev/null +++ b/miniprogram_npm/weapp-qrcode/index.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["weapp.qrcode.common.js"],"names":[],"mappings":";;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA","file":"index.js","sourcesContent":["/**\n * weapp.qrcode.js v1.0.0 (https://github.com/yingye/weapp-qrcode#readme)\n */\n\nvar 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++}},module.exports=drawQrcode;\n"]} \ No newline at end of file diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json new file mode 100644 index 0000000..bafad55 --- /dev/null +++ b/node_modules/.package-lock.json @@ -0,0 +1,33 @@ +{ + "name": "miniapp", + "lockfileVersion": 3, + "requires": true, + "packages": { + "node_modules/@jdmini/api": { + "version": "1.0.10", + "resolved": "https://registry.npmmirror.com/@jdmini/api/-/api-1.0.10.tgz", + "integrity": "sha512-bVFU0awuY033mUT4QqArrYbrnPkBaBFKHoqCMHTVnRCk4b6gTs+cCGDH8uyf2t8ybCgWITKxaaH4Vjzyq8VF8g==" + }, + "node_modules/@jdmini/components": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/@jdmini/components/-/components-1.0.6.tgz", + "integrity": "sha512-ndva1nlZ1QJqDVgHfB0GPxMGmXsZ7SbWjUkRm/WoQIkow75fFbaQCW/xhtQQ+bPbJLjXmCg2p2356klsLLib8A==", + "peerDependencies": { + "@jdmini/api": ">=1.0.8" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/weapp-qrcode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/weapp-qrcode/-/weapp-qrcode-1.0.0.tgz", + "integrity": "sha512-4sa3W0rGDVJ9QaeZpAKlAuUxVyjhDwiUqHyGK/jJMsRMXnhb4yO8qWU/pZruMo+iT5J6CraS67lDMFb1VY+RaA==", + "dependencies": { + "extend": "^3.0.2" + } + } + } +} diff --git a/node_modules/@jdmini/api/README.md b/node_modules/@jdmini/api/README.md new file mode 100644 index 0000000..a82f0ec --- /dev/null +++ b/node_modules/@jdmini/api/README.md @@ -0,0 +1,252 @@ +## 安装/更新 + +1、终端使用命令安装 npm 包 + +```bash +npm install --save @jdmini/api@latest +``` + +> ```bash +> npm install --save @jdmini/components@latest +> ``` + +2、在小程序开发者工具中:菜单选择工具 -> 构建 npm + +## 使用 + +```js +import { + onLoginReady, + waitLogin, + + injectApp, + injectPage, + injectComponent, + hijackApp, + hijackAllPage, + + gatewayHttpClient, + baseHttpClient, + apiHttpClient, + HttpClient, + + adManager, +} from '@jdmini/api' +``` + +### `waitLogin`/`onLoginReady` - 确保登录完成 + +- 同步的写法 + +```js +async function onLoad() { + await waitLogin() + // 此处代码将在已登录或登陆完成后执行。请求将自动携带Token + await gatewayHttpClient.request('/xxx', 'GET', {}) +} +``` + +- 异步的写法 + +```js +function onLoad() { + onLoginReady(() => { + // 此处代码将在已登录或登陆完成后执行。请求将自动携带Token + gatewayHttpClient.request('/xxx', 'GET', {}) + }) +} +``` + +### `injectApp` - 向App注入基础代码 + +- 注入之后实现自动登录、广告初始化等功能 + +```js +// app.js +App(injectApp()({ + // 业务代码 + onLaunch() { + + } +})) +``` + +### `injectPage` - 向Page注入基础代码 + +- 注入之后实现页面自动统计、自动展示插屏广告以及激励视频广告的调用支持 +- 参数: + - showInterstitialAd: 是否自动展示插屏广告 + +```js +// pages/xxx/xxx.js +Page(injectPage({ + showInterstitialAd: true +})({ + // 业务代码 + onLoad() { + + } +})) +``` + +### `injectComponent` - 向Component注入基础代码 + +- 适用于使用Component构造页面的场景 +- 注入之后实现页面自动统计、自动展示插屏广告以及激励视频广告的调用支持 +- 参数: + - showInterstitialAd: 是否自动展示插屏广告 + +```js +// pages/xxx/xxx.js +Component(injectComponent({ + showInterstitialAd: true +})({ + // 业务代码 + methods: { + onLoad() { + + } + } +})) +``` + +### `hijackApp` - 劫持全局App方法,注入基础代码 + +- 在不方便使用injectApp时使用(如解包后代码复杂,难以修改App调用) +- 此方法会修改全局App方法,存在未知风险,使用时请进行完整测试 +- 不可与injectApp同时使用 + +```js +// app.js +hijackApp() +``` + +### `hijackAllPage` - 劫持全局Page方法,注入基础代码 + +- 在不方便使用injectPage/injectComponent时使用(如解包后代码复杂,难以修改Page/Component调用) +- 此方法会修改全局Page方法,并影响所有的页面,存在未知风险,使用时请进行完整测试 +- 参数同injectPage/injectComponent方法,不可与这些方法同时使用 + +```js +// app.js +hijackAllPage({ + showInterstitialAd: true +}) +``` + +### `gatewayHttpClient` - 网关API调用封装 + +- 同步的写法 + +```js +async function onLoad() { + try { + // 网关请求。参数:路径、方法、数据、其他选项(如headers、responseType) + const data = await gatewayHttpClient.request(path, method, data,options) + + // 头像上传。参数:文件路径 + const data = await gatewayHttpClient.uploadAvatar(filePath) + + // 文件上传。参数:文件路径、数据 + const data = await gatewayHttpClient.uploadFile(filePath, data) + + // 文件删除。参数:文件ID + const data = await gatewayHttpClient.deleteFile(fileId) + } catch(err) { + // 响应HTTP状态码非200时自动showToast并抛出异常 + } +} +``` + +- 所有方法均支持异步的写法 + +```js +function onLoad() { + gatewayHttpClient.request('/xxx') + .then(data => { + console.log(data) + }) + .catch(err => {}) +} +``` + +### `baseHttpClient`/`apiHttpClient` - 为老版本兼容保留,不推荐使用 + +### `HttpClient` - API底层类,用于封装自定义请求 + +- 示例:封装一个百度的请求客户端,并调用百度搜索 + +```js +const baiduHttpClient = new HttpClient({ + baseURL: 'https://www.baidu.com', + timeout: 5000, +}); + +baiduHttpClient.request('/s', 'GET', { wd: '测试' }, { responseType: 'text' }) + .then(data => console.log(data)) +``` + +### `adManager` - 广告管理器 + +- 确保广告数据加载完成,支持同步/异步的写法 + +```js +// 同步的写法 +async function onLoad() { + await adManager.waitAdData() + // 此处代码将在广告数据加载完成后执行 + await adManager.createAndShowInterstitialAd() +} + +// 异步的写法 +function onLoad () { + adManager.onDataReady(() => { + // 此处代码将在广告数据加载完成后执行 + adManager.createAndShowInterstitialAd() + }) +} +``` + +- 广告数据 + +```js +// 格式化之后的广告数据对象,如{banner: "adunit-f7709f349de05edc", custom: "adunit-34c76b0c3e4a6ed0", ...} +const ads = adManager.ads + +// 友情链接顶部广告数据 +const top = adManager.top + +// 友情链接数据 +const link = adManager.link +``` + +- 创建并展示插屏广告 + +```js +function onLoad() { + adManager.createAndShowInterstitialAd() +} +``` + +- 创建并展示激励视频广告 + - 传入当前页面的上下文this,返回用户是否已看完广告 + - 由于微信的底层限制,需要先在调用的页面上进行injectPage注入,且该方法必须放在用户的点击事件里调用 + - 使用示例可参考[jdwx-demo](https://code.miniappapi.com/wx/jdwx-demo) + +```js +// 同步的写法 +page({ + async handleClick() { + const isEnded = await adManager.createAndShowRewardedVideoAd(this) + } +}) + +// 异步的写法 +page({ + handleClick() { + adManager.createAndShowRewardedVideoAd(this).then((isEnded) => { + + }) + } +}) +``` diff --git a/node_modules/@jdmini/api/miniprogram_dist/index.d.ts b/node_modules/@jdmini/api/miniprogram_dist/index.d.ts new file mode 100644 index 0000000..e06e58d --- /dev/null +++ b/node_modules/@jdmini/api/miniprogram_dist/index.d.ts @@ -0,0 +1,295 @@ +// Generated by dts-bundle v0.7.3 + +declare module '@jdmini/api' { + import { onLoginReady, waitLogin } from '@jdmini/api/app'; + import HttpClient, { gatewayHttpClient, baseHttpClient, apiHttpClient } from '@jdmini/api/httpClient'; + import { injectApp, injectPage, injectComponent, hijackApp, hijackAllPage } from '@jdmini/api/injector'; + import adManager from '@jdmini/api/adManager'; + export { onLoginReady, waitLogin, injectApp, injectPage, injectComponent, hijackApp, hijackAllPage, gatewayHttpClient, baseHttpClient, apiHttpClient, HttpClient, adManager, }; +} + +declare module '@jdmini/api/app' { + export interface AppOptions { + gatewayUrl?: string; + baseUrl?: string; + apiUrl?: string; + } + export interface PageOptions { + showInterstitialAd?: boolean; + } + export function initApp(options?: AppOptions): Promise; + export function showPage(options: PageOptions | undefined, pageId: string): Promise; + export const checkTokenValid: () => boolean; + /** + * 确保登录完成 + * @param {Function} callback - 回调函数 + * @returns {void} + */ + export function onLoginReady(callback: (...args: any[]) => void): void; + /** + * 等待登录完成 + * @returns {Promise} + */ + export function waitLogin(): Promise; + export function login(): Promise; + export function fetchEchoData(): Promise; + export function trackVisit(): Promise; +} + +declare module '@jdmini/api/httpClient' { + import { HttpClientOptions, RequestOptions, ApiResponse } from '@jdmini/api/types'; + class HttpClient { + constructor({ baseURL, timeout }: HttpClientOptions); + setBaseURL(baseURL: string): void; + /** + * 请求 + * @param {string} path 路径 + * @param {string} method 方法, 默认GET + * @param {Object} data 数据, 默认{} + * @param {Object} options 传入wx.request的其他配置, 默认{} + * @returns {Promise} 返回一个Promise对象 + */ + request(path: string, method?: WechatMiniprogram.RequestOption['method'], data?: Record, options?: RequestOptions): Promise>; + /** + * 上传文件 + * @param {string} filePath 文件路径 + * @param {Object} data 数据, 默认{} + * @param {'avatar' | 'file'} type 类型, 默认'file' + * @returns {Promise} 返回一个Promise对象 + */ + uploadFile(filePath: string, data?: Record, type?: 'avatar' | 'file'): Promise>; + /** + * 上传文件 + * @param {string} filePath 文件路径 + * @param {Object} data 数据, 默认{} + * @param {'avatar' | 'file'} type 类型, 默认'file' + * @returns {Promise} 返回一个Promise对象 + */ + upload(path: string, filePath: string, data?: Record): Promise>; + /** + * 删除文件 + * @param {number} fileId 文件id + * @returns {Promise} 返回一个Promise对象 + */ + deleteFile(fileId: number): Promise>; + /** + * 上传头像 + * @param {string} filePath 文件路径 + * @returns {Promise} 返回一个Promise对象 + */ + uploadAvatar(filePath: string): Promise>; + } + export const gatewayHttpClient: HttpClient; + export const baseHttpClient: HttpClient; + export const apiHttpClient: HttpClient; + export default HttpClient; +} + +declare module '@jdmini/api/injector' { + interface AppConfig { + onLaunch?: (...args: any[]) => void | Promise; + [key: string]: any; + } + interface PageConfig { + onShow?: (...args: any[]) => void | Promise; + [key: string]: any; + } + interface ComponentConfig { + methods?: { + onLoad?: (...args: any[]) => void | Promise; + onShow?: (...args: any[]) => void | Promise; + [key: string]: any; + }; + [key: string]: any; + } + interface InjectAppOptions { + gatewayUrl?: string; + baseUrl?: string; + apiUrl?: string; + } + interface InjectPageOptions { + showInterstitialAd?: boolean; + } + /** + * 注入应用配置 + * @param {Object} options - 配置选项 + * @param {string} [options.gatewayUrl] - 网关地址,默认使用CONFIG.API.GATEWAY_URL + * @param {string} [options.baseUrl] - 基础地址,默认使用CONFIG.API.BASE_URL + * @param {string} [options.apiUrl] - api地址,默认使用CONFIG.API.API_URL + * @returns {Function} 返回一个接收应用配置的函数 + */ + export function injectApp(options?: InjectAppOptions): (appConfig: AppConfig) => AppConfig; + /** + * 注入页面配置 + * @param {InjectPageOptions} options - 配置选项 + * @param {boolean} [options.showInterstitialAd] - 是否在onShow显示插屏广告,默认不显示 + * @returns {Function} 返回一个接收页面配置的函数 + */ + export function injectPage(options?: InjectPageOptions): (pageConfig?: PageConfig) => PageConfig; + /** + * 注入组件配置 + * @param {InjectPageOptions} options - 配置选项 + * @param {boolean} [options.showInterstitialAd] - 是否在onShow显示插屏广告,默认不显示 + * @returns {Function} 返回一个接收组件配置的函数 + */ + export function injectComponent(options?: InjectPageOptions): (pageConfig?: PageConfig) => ComponentConfig; + /** + * 劫持App + * @param {InjectAppOptions} options - 配置选项 + * @param {string} [options.gatewayUrl] - 网关地址,默认使用CONFIG.API.GATEWAY_URL + * @param {string} [options.baseUrl] - 基础地址,默认使用CONFIG.API.BASE_URL + * @param {string} [options.apiUrl] - api地址,默认使用CONFIG.API.API_URL + * @returns {void} + */ + export const hijackApp: (options?: InjectAppOptions) => void; + /** + * 劫持所有Page + * @param {InjectPageOptions} options - 配置选项 + * @param {boolean} [options.showInterstitialAd] - 是否在onShow显示插屏广告,默认不显示 + * @returns {void} + */ + export const hijackAllPage: (options?: InjectPageOptions) => void; + export {}; +} + +declare module '@jdmini/api/adManager' { + import { AdData, LinkData, TopData } from '@jdmini/api/types'; + type Ads = Partial>; + class AdManager { + /** + * 广告数据 + */ + ads: Ads; + /** + * 友情链接数据 + */ + link: LinkData[]; + /** + * 友情链接顶部广告数据 + */ + top: TopData | null; + constructor(); + /** + * 确保广告数据就绪 + * @param {Function} callback - 回调函数 + * @returns {void} + */ + onDataReady: (callback: (...args: any[]) => void) => void; + /** + * 等待广告数据加载完成 + * @returns {Promise} + */ + waitAdData: () => Promise; + /** + * 初始化广告数据 + * @returns {void} + */ + init: () => void; + /** + * 创建并展示插屏广告 + * @returns {Promise} + */ + createAndShowInterstitialAd: () => Promise; + /** + * 创建并展示激励视频广告 + * @param {any} context - 页面上下文 + * @param {string} [pageId] - 页面ID + * @returns {Promise} 是否完成播放 + */ + createAndShowRewardedVideoAd: (context: any, pageId?: string) => Promise; + } + const _default: AdManager; + export default _default; +} + +declare module '@jdmini/api/types' { + export interface Config { + API: { + GATEWAY_URL: string; + BASE_URL: string; + API_URL: string; + }; + APP: { + APP_ID: number; + LOGIN_MAX_RETRY: number; + }; + HTTP: { + TIMEOUT: number; + }; + DATA: { + PAGE_ID: string; + }; + STORAGE_KEYS: { + TOKEN: string; + USER_INFO: string; + SPA_DATA: string; + LINK_DATA: string; + TOP_DATA: string; + }; + EVENT_KEYS: { + LOGIN_SUCCESS: string; + AD_DATA_READY: string; + REWARDED_VIDEO_AD_CLOSE: string; + }; + } + export interface HttpClientOptions { + baseURL: string; + timeout?: number; + } + export interface RequestOptions { + headers?: Record; + [key: string]: any; + } + export interface LoginData { + appId: number; + code: string; + brand: string; + model: string; + platform: string; + } + export interface ApiResponse { + code: number; + message?: string; + data?: T; + } + export interface UserInfo { + token: string; + user: { + id: number; + name: string; + avatar: string; + openId: string; + }; + } + export interface EchoData { + isPublished: boolean; + spads: AdData[]; + links: LinkData[]; + top: TopData; + version: number; + } + export interface AdData { + id: number; + status: number; + appPage: 'banner' | 'custom' | 'video' | 'interstitial' | 'rewarded'; + ads: { + id: number; + type: number; + adId: number; + adUnitId: string; + }[]; + } + export interface LinkData { + appId: string; + appLogo: string; + linkName: string; + linkPage: string; + } + export interface TopData { + appId: string; + appLogo: string; + linkName: string; + appDsc: string; + } +} + diff --git a/node_modules/@jdmini/api/miniprogram_dist/index.js b/node_modules/@jdmini/api/miniprogram_dist/index.js new file mode 100644 index 0000000..fc11843 --- /dev/null +++ b/node_modules/@jdmini/api/miniprogram_dist/index.js @@ -0,0 +1,4 @@ +/*! + * @jdmini/api v1.0.10 + * + */(()=>{"use strict";var t={616:function(t,e,n){var r=this&&this.__awaiter||function(t,e,n,r){return new(n||(n=Promise))((function(o,a){function i(t){try{u(r.next(t))}catch(t){a(t)}}function s(t){try{u(r.throw(t))}catch(t){a(t)}}function u(t){var e;t.done?o(t.value):(e=t.value,e instanceof n?e:new n((function(t){t(e)}))).then(i,s)}u((r=r.apply(t,e||[])).next())}))},o=this&&this.__generator||function(t,e){var n,r,o,a={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]},i=Object.create(("function"==typeof Iterator?Iterator:Object).prototype);return i.next=s(0),i.throw=s(1),i.return=s(2),"function"==typeof Symbol&&(i[Symbol.iterator]=function(){return this}),i;function s(s){return function(u){return function(s){if(n)throw new TypeError("Generator is already executing.");for(;i&&(i=0,s[0]&&(a=0)),a;)try{if(n=1,r&&(o=2&s[0]?r.return:s[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,s[1])).done)return o;switch(r=0,o&&(s=[2&s[0],o.value]),s[0]){case 0:case 1:o=s;break;case 4:return a.label++,{value:s[1],done:!1};case 5:a.label++,r=s[1],s=[0];continue;case 7:s=a.ops.pop(),a.trys.pop();continue;default:if(!(o=a.trys,(o=o.length>0&&o[o.length-1])||6!==s[0]&&2!==s[0])){a=0;continue}if(3===s[0]&&(!o||s[1]>o[0]&&s[1]0?e.ads[0].adUnitId:"",t}),{}));var n=wx.getStorageSync(i.default.STORAGE_KEYS.LINK_DATA);n&&(t.link=n);var r=wx.getStorageSync(i.default.STORAGE_KEYS.TOP_DATA);r&&r.appDsc.length>40&&(r.appDsc=r.appDsc.substring(0,40)+"...",t.top=r),t.adDataReady=!0,s.default.emit(i.default.EVENT_KEYS.AD_DATA_READY)},this.createAndShowInterstitialAd=function(){return r(t,void 0,void 0,(function(){var t;return o(this,(function(e){switch(e.label){case 0:return this.interstitialAd&&this.interstitialAd.destroy(),[4,this.waitAdData()];case 1:e.sent(),e.label=2;case 2:return e.trys.push([2,5,,6]),[4,this.createInterstitialAd()];case 3:return e.sent(),[4,this.showInterstitialAd()];case 4:return e.sent(),[3,6];case 5:return t=e.sent(),console.error("创建插屏广告失败:",t),[3,6];case 6:return[2]}}))}))},this.createInterstitialAd=function(){return r(t,void 0,void 0,(function(){var t=this;return o(this,(function(e){return[2,new Promise((function(e,n){t.ads.interstitial?(t.interstitialAd=wx.createInterstitialAd({adUnitId:t.ads.interstitial}),t.interstitialAd.onLoad((function(){console.log("插屏广告加载成功"),e()})),t.interstitialAd.onError((function(t){console.error(t),n(new Error("插屏广告加载失败"))})),t.interstitialAd.onClose((function(){console.log("插屏广告关闭")}))):n(new Error("插屏广告未配置"))}))]}))}))},this.showInterstitialAd=function(){return r(t,void 0,void 0,(function(){var t,e;return o(this,(function(n){switch(n.label){case 0:return n.trys.push([0,2,,3]),console.log("开始展示插屏广告"),[4,null===(e=this.interstitialAd)||void 0===e?void 0:e.show()];case 1:return[2,n.sent()];case 2:return t=n.sent(),console.error("插屏广告展示失败:",t),[3,3];case 3:return[2]}}))}))},this.createAndShowRewardedVideoAd=function(e,n){return r(t,void 0,void 0,(function(){var t,r,a;return o(this,(function(o){switch(o.label){case 0:return[4,this.waitAdData()];case 1:o.sent(),t=n||(null===(a=null==e?void 0:e.data)||void 0===a?void 0:a[i.default.DATA.PAGE_ID]),o.label=2;case 2:if(o.trys.push([2,6,,7]),!t)throw new Error("未指定pageId或者context");return this.rewardedVideoAds[t]?[3,4]:[4,this.createRewardedVideoAd(t)];case 3:o.sent(),o.label=4;case 4:return[4,this.showRewardedVideoAd(t)];case 5:return o.sent(),[3,7];case 6:return r=o.sent(),console.error("创建激励视频广告失败:",r),[3,7];case 7:return[2,new Promise((function(e){s.default.on(i.default.EVENT_KEYS.REWARDED_VIDEO_AD_CLOSE,(function(n,r){n===t&&e(r)}))}))]}}))}))},this.createRewardedVideoAd=function(e){return r(t,void 0,void 0,(function(){var t=this;return o(this,(function(n){return[2,new Promise((function(n,r){t.ads.rewarded?(t.rewardedVideoAds[e]=wx.createRewardedVideoAd({adUnitId:t.ads.rewarded}),t.rewardedVideoAds[e].onLoad((function(){console.log("激励视频广告加载成功"),n()})),t.rewardedVideoAds[e].onError((function(t){console.error(t),r(new Error("激励视频广告加载失败"))})),t.rewardedVideoAds[e].onClose((function(t){s.default.emit(i.default.EVENT_KEYS.REWARDED_VIDEO_AD_CLOSE,e,t.isEnded),console.log("激励视频广告关闭")}))):r(new Error("激励视频广告未配置"))}))]}))}))},this.showRewardedVideoAd=function(e){return r(t,void 0,void 0,(function(){var t,n;return o(this,(function(r){switch(r.label){case 0:return r.trys.push([0,2,,3]),console.log("开始展示激励视频广告"),[4,null===(n=this.rewardedVideoAds[e])||void 0===n?void 0:n.show()];case 1:return[2,r.sent()];case 2:return t=r.sent(),console.error("激励视频广告展示失败:",t),[3,3];case 3:return[2]}}))}))}};e.default=new u},859:function(t,e,n){var r=this&&this.__awaiter||function(t,e,n,r){return new(n||(n=Promise))((function(o,a){function i(t){try{u(r.next(t))}catch(t){a(t)}}function s(t){try{u(r.throw(t))}catch(t){a(t)}}function u(t){var e;t.done?o(t.value):(e=t.value,e instanceof n?e:new n((function(t){t(e)}))).then(i,s)}u((r=r.apply(t,e||[])).next())}))},o=this&&this.__generator||function(t,e){var n,r,o,a={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]},i=Object.create(("function"==typeof Iterator?Iterator:Object).prototype);return i.next=s(0),i.throw=s(1),i.return=s(2),"function"==typeof Symbol&&(i[Symbol.iterator]=function(){return this}),i;function s(s){return function(u){return function(s){if(n)throw new TypeError("Generator is already executing.");for(;i&&(i=0,s[0]&&(a=0)),a;)try{if(n=1,r&&(o=2&s[0]?r.return:s[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,s[1])).done)return o;switch(r=0,o&&(s=[2&s[0],o.value]),s[0]){case 0:case 1:o=s;break;case 4:return a.label++,{value:s[1],done:!1};case 5:a.label++,r=s[1],s=[0];continue;case 7:s=a.ops.pop(),a.trys.pop();continue;default:if(!(o=a.trys,(o=o.length>0&&o[o.length-1])||6!==s[0]&&2!==s[0])){a=0;continue}if(3===s[0]&&(!o||s[1]>o[0]&&s[1]s.default.APP.LOGIN_MAX_RETRY)throw wx.showToast({title:"网络异常,无法初始化",icon:"none",duration:2e3}),new Error("网络异常,无法初始化");o.label=1;case 1:return o.trys.push([1,7,,9]),t=wx.getDeviceInfo(),[4,wx.login()];case 2:return e=o.sent().code,n={appId:s.default.APP.APP_ID,code:e,brand:t.brand,model:t.model,platform:t.platform},[4,i.gatewayHttpClient.request("/wx/v1/api/login","POST",n)];case 3:return 200===(r=o.sent()).code&&r.data?(wx.setStorageSync(s.default.STORAGE_KEYS.TOKEN,r.data.token),wx.setStorageSync(s.default.STORAGE_KEYS.USER_INFO,r.data.user),l=0,u.default.emit(s.default.EVENT_KEYS.LOGIN_SUCCESS),[3,6]):[3,4];case 4:return l++,[4,h()];case 5:o.sent(),o.label=6;case 6:return[3,9];case 7:return o.sent(),l++,[4,h()];case 8:return o.sent(),[3,9];case 9:return[2]}}))}))}function p(){return r(this,void 0,void 0,(function(){var t;return o(this,(function(n){switch(n.label){case 0:return wx.removeStorageSync(s.default.STORAGE_KEYS.SPA_DATA),wx.removeStorageSync(s.default.STORAGE_KEYS.LINK_DATA),wx.removeStorageSync(s.default.STORAGE_KEYS.TOP_DATA),(0,e.checkTokenValid)()?[4,i.gatewayHttpClient.request("/wx/v1/api/echo","GET")]:[2];case 1:return 200===(t=n.sent()).code&&t.data?(t.data.spads&&wx.setStorageSync(s.default.STORAGE_KEYS.SPA_DATA,t.data.spads),t.data.links&&wx.setStorageSync(s.default.STORAGE_KEYS.LINK_DATA,t.data.links),t.data.top&&wx.setStorageSync(s.default.STORAGE_KEYS.TOP_DATA,t.data.top),[3,5]):[3,2];case 2:return 401!==t.code?[3,5]:[4,h()];case 3:return n.sent(),[4,p()];case 4:n.sent(),n.label=5;case 5:return[2]}}))}))}function w(){return r(this,void 0,void 0,(function(){var t;return o(this,(function(e){switch(e.label){case 0:return e.trys.push([0,5,,6]),[4,i.gatewayHttpClient.request("/wx/v1/api/visit","POST")];case 1:return 401!==e.sent().code?[3,4]:[4,h()];case 2:return e.sent(),[4,w()];case 3:e.sent(),e.label=4;case 4:return[3,6];case 5:return t=e.sent(),console.error("访问统计失败:",t),[3,6];case 6:return[2]}}))}))}e.checkTokenValid=function(){var t=wx.getStorageSync(s.default.STORAGE_KEYS.TOKEN);return!(!t||t.length<32)}},28:(t,e)=>{Object.defineProperty(e,"__esModule",{value:!0});var n={API:{GATEWAY_URL:"https://ca.miniappapi.com",BASE_URL:"https://app.jd027.com/v1/api",API_URL:"https://cp.miniappapi.com"},APP:{APP_ID:wx.getExtConfigSync().appId||313,LOGIN_MAX_RETRY:2},HTTP:{TIMEOUT:5e3},DATA:{PAGE_ID:"jdwx-page-id"},STORAGE_KEYS:{TOKEN:"jdwx-token",USER_INFO:"jdwx-userinfo",SPA_DATA:"jdwx-spadata",LINK_DATA:"jdwx-linkdata",TOP_DATA:"jdwx-topdata"},EVENT_KEYS:{LOGIN_SUCCESS:"jdwx-login-success",AD_DATA_READY:"jdwx-ad-data-ready",REWARDED_VIDEO_AD_CLOSE:"jdwx-rewarded-video-ad-close"}};e.default=n},144:(t,e)=>{Object.defineProperty(e,"__esModule",{value:!0});var n=function(){function t(){this.events={}}return t.prototype.on=function(t,e){if("string"!=typeof t)throw new TypeError("eventName must be a string");if("function"!=typeof e)throw new TypeError("callback must be a function");return this.events[t]||(this.events[t]=[]),this.events[t].push(e),this},t.prototype.emit=function(t){for(var e=[],n=1;n0&&o[o.length-1])||6!==s[0]&&2!==s[0])){a=0;continue}if(3===s[0]&&(!o||s[1]>o[0]&&s[1]=200&&d.statusCode<300)return[2,d.data];if(401===d.statusCode)return[2,{code:401,message:"未授权"}];throw new Error(d.data.message||"请求失败");case 4:throw f=a.sent(),console.error("网络错误:",f),wx.showToast({title:f instanceof Error?f.message:"网络错误",icon:"none",duration:2e3}),f;case 5:return[2]}}))}))},t.prototype.uploadFile=function(t){return o(this,arguments,void 0,(function(t,e,n){var r,o,i,u;return void 0===e&&(e={}),void 0===n&&(n="file"),a(this,(function(a){r=this.baseURL===s.default.API.GATEWAY_URL,o=wx.getStorageSync(s.default.STORAGE_KEYS.TOKEN),i="avatar"===n?"/avatar":"/file/new",u=this.joinURL(this.baseURL,"".concat(r?"/wx/v1/api":"").concat(i));try{return[2,new Promise((function(n,r){wx.uploadFile({url:u,name:"file",filePath:t,formData:e,header:{"Content-Type":"application/x-www-form-urlencoded",Authorization:o},success:function(t){if(t.statusCode>=200&&t.statusCode<300)n(JSON.parse(t.data));else{if(401!==t.statusCode)throw new Error(t.data.message||"上传失败");n(JSON.parse(t.data))}},fail:function(){throw new Error("网络错误")}}).onProgressUpdate((function(t){console.log("上传进度",t.progress),console.log("已经上传的数据长度",t.totalBytesSent),console.log("预期需要上传的数据总长度",t.totalBytesExpectedToSend)}))}))]}catch(t){throw console.error("上传失败:",t),wx.showToast({title:t instanceof Error?t.message:"上传失败",icon:"none",duration:2e3}),t}return[2]}))}))},t.prototype.upload=function(t,e){return o(this,arguments,void 0,(function(t,e,n){var r,o,i;return void 0===n&&(n={}),a(this,(function(a){r=this.baseURL===s.default.API.GATEWAY_URL,o=wx.getStorageSync(s.default.STORAGE_KEYS.TOKEN),i=this.joinURL(this.baseURL,"".concat(r?"/wx/v1/api":"").concat(t));try{return[2,new Promise((function(t,r){wx.uploadFile({url:i,name:"file",filePath:e,formData:n,header:{"Content-Type":"application/x-www-form-urlencoded",Authorization:o},success:function(e){if(e.statusCode>=200&&e.statusCode<300)t(JSON.parse(e.data));else{if(401!==e.statusCode)throw new Error(e.data.message||"上传失败");t(JSON.parse(e.data))}},fail:function(){throw new Error("网络错误")}}).onProgressUpdate((function(t){console.log("上传进度",t.progress),console.log("已经上传的数据长度",t.totalBytesSent),console.log("预期需要上传的数据总长度",t.totalBytesExpectedToSend)}))}))]}catch(t){throw console.error("上传失败:",t),wx.showToast({title:t instanceof Error?t.message:"上传失败",icon:"none",duration:2e3}),t}return[2]}))}))},t.prototype.deleteFile=function(t){return o(this,void 0,void 0,(function(){var e;return a(this,(function(n){return e=this.baseURL===s.default.API.GATEWAY_URL,[2,this.request("".concat(e?"/wx/v1/api":"","/file/del"),"GET",{id:t})]}))}))},t.prototype.uploadAvatar=function(t){return o(this,void 0,void 0,(function(){return a(this,(function(e){return[2,this.uploadFile(t,{},"avatar")]}))}))},t}();e.gatewayHttpClient=new u({baseURL:s.default.API.GATEWAY_URL,timeout:s.default.HTTP.TIMEOUT}),e.baseHttpClient=new u({baseURL:s.default.API.BASE_URL,timeout:s.default.HTTP.TIMEOUT}),e.apiHttpClient=new u({baseURL:s.default.API.API_URL,timeout:s.default.HTTP.TIMEOUT}),e.default=u},156:function(t,e,n){var r=this&&this.__createBinding||(Object.create?function(t,e,n,r){void 0===r&&(r=n);var o=Object.getOwnPropertyDescriptor(e,n);o&&!("get"in o?!e.__esModule:o.writable||o.configurable)||(o={enumerable:!0,get:function(){return e[n]}}),Object.defineProperty(t,r,o)}:function(t,e,n,r){void 0===r&&(r=n),t[r]=e[n]}),o=this&&this.__setModuleDefault||(Object.create?function(t,e){Object.defineProperty(t,"default",{enumerable:!0,value:e})}:function(t,e){t.default=e}),a=this&&this.__importStar||function(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var n in t)"default"!==n&&Object.prototype.hasOwnProperty.call(t,n)&&r(e,t,n);return o(e,t),e},i=this&&this.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(e,"__esModule",{value:!0}),e.adManager=e.HttpClient=e.apiHttpClient=e.baseHttpClient=e.gatewayHttpClient=e.hijackAllPage=e.hijackApp=e.injectComponent=e.injectPage=e.injectApp=e.waitLogin=e.onLoginReady=void 0;var s=n(859);Object.defineProperty(e,"onLoginReady",{enumerable:!0,get:function(){return s.onLoginReady}}),Object.defineProperty(e,"waitLogin",{enumerable:!0,get:function(){return s.waitLogin}});var u=a(n(161));e.HttpClient=u.default,Object.defineProperty(e,"gatewayHttpClient",{enumerable:!0,get:function(){return u.gatewayHttpClient}}),Object.defineProperty(e,"baseHttpClient",{enumerable:!0,get:function(){return u.baseHttpClient}}),Object.defineProperty(e,"apiHttpClient",{enumerable:!0,get:function(){return u.apiHttpClient}});var c=n(718);Object.defineProperty(e,"injectApp",{enumerable:!0,get:function(){return c.injectApp}}),Object.defineProperty(e,"injectPage",{enumerable:!0,get:function(){return c.injectPage}}),Object.defineProperty(e,"injectComponent",{enumerable:!0,get:function(){return c.injectComponent}}),Object.defineProperty(e,"hijackApp",{enumerable:!0,get:function(){return c.hijackApp}}),Object.defineProperty(e,"hijackAllPage",{enumerable:!0,get:function(){return c.hijackAllPage}});var l=i(n(616));e.adManager=l.default},718:function(t,e,n){var r=this&&this.__assign||function(){return r=Object.assign||function(t){for(var e,n=1,r=arguments.length;n 构建 npm + +`注意:依赖@jdmini/api,请确保小程序项目已安装@jdmini/api` + +## 使用 + +1、在页面的 json 文件中引入组件: + +```json +{ + "usingComponents": { + "jdwx-ad": "@jdmini/components/jdwx-ad", + "jdwx-link": "@jdmini/components/jdwx-link" + } +} +``` + +2、在页面的 wxml 文件中使用组件: + +```html + + +``` diff --git a/node_modules/@jdmini/components/miniprogram_dist/icons/home-active.png b/node_modules/@jdmini/components/miniprogram_dist/icons/home-active.png new file mode 100644 index 0000000..127b0ed Binary files /dev/null and b/node_modules/@jdmini/components/miniprogram_dist/icons/home-active.png differ diff --git a/node_modules/@jdmini/components/miniprogram_dist/icons/home.png b/node_modules/@jdmini/components/miniprogram_dist/icons/home.png new file mode 100644 index 0000000..11016da Binary files /dev/null and b/node_modules/@jdmini/components/miniprogram_dist/icons/home.png differ diff --git a/node_modules/@jdmini/components/miniprogram_dist/icons/link-active.png b/node_modules/@jdmini/components/miniprogram_dist/icons/link-active.png new file mode 100644 index 0000000..433ec7b Binary files /dev/null and b/node_modules/@jdmini/components/miniprogram_dist/icons/link-active.png differ diff --git a/node_modules/@jdmini/components/miniprogram_dist/icons/link.png b/node_modules/@jdmini/components/miniprogram_dist/icons/link.png new file mode 100644 index 0000000..c021ab7 Binary files /dev/null and b/node_modules/@jdmini/components/miniprogram_dist/icons/link.png differ diff --git a/node_modules/@jdmini/components/miniprogram_dist/jdwx-ad/index.js b/node_modules/@jdmini/components/miniprogram_dist/jdwx-ad/index.js new file mode 100644 index 0000000..19671f8 --- /dev/null +++ b/node_modules/@jdmini/components/miniprogram_dist/jdwx-ad/index.js @@ -0,0 +1,22 @@ +import { adManager } from '@jdmini/api' + +Component({ + properties: { + type: { + type: String, + value: 'custom' // 可选banner, video, custom + } + }, + data: { + ads: {} + }, + lifetimes: { + attached: function () { + adManager.onDataReady(() => { + this.setData({ ads: adManager.ads }) + }) + }, + }, + methods: { + } +}) diff --git a/node_modules/@jdmini/components/miniprogram_dist/jdwx-ad/index.json b/node_modules/@jdmini/components/miniprogram_dist/jdwx-ad/index.json new file mode 100644 index 0000000..32640e0 --- /dev/null +++ b/node_modules/@jdmini/components/miniprogram_dist/jdwx-ad/index.json @@ -0,0 +1,3 @@ +{ + "component": true +} \ No newline at end of file diff --git a/node_modules/@jdmini/components/miniprogram_dist/jdwx-ad/index.wxml b/node_modules/@jdmini/components/miniprogram_dist/jdwx-ad/index.wxml new file mode 100644 index 0000000..1b24adf --- /dev/null +++ b/node_modules/@jdmini/components/miniprogram_dist/jdwx-ad/index.wxml @@ -0,0 +1,5 @@ + + + + + diff --git a/node_modules/@jdmini/components/miniprogram_dist/jdwx-ad/index.wxss b/node_modules/@jdmini/components/miniprogram_dist/jdwx-ad/index.wxss new file mode 100644 index 0000000..de627db --- /dev/null +++ b/node_modules/@jdmini/components/miniprogram_dist/jdwx-ad/index.wxss @@ -0,0 +1,7 @@ +.jdwx-ad-component { + padding: 10rpx; +} + +.jdwx-ad-item { + bottom: 10rpx; +} \ No newline at end of file diff --git a/node_modules/@jdmini/components/miniprogram_dist/jdwx-link/index.js b/node_modules/@jdmini/components/miniprogram_dist/jdwx-link/index.js new file mode 100644 index 0000000..887d749 --- /dev/null +++ b/node_modules/@jdmini/components/miniprogram_dist/jdwx-link/index.js @@ -0,0 +1,37 @@ +import { adManager } from '@jdmini/api' + +Component({ + properties: { + }, + data: { + link: [], + top: {}, + }, + pageLifetimes: { + show: function () { + adManager.onDataReady(() => { + this.setData({ + link: adManager.link, + top: adManager.top + }) + }) + }, + }, + methods: { + gotopLink: function () { + wx.vibrateShort() + wx.openEmbeddedMiniProgram({ + appId: this.data.top.appId, + path: this.data.top.linkPage + }); + }, + goLink: function (e) { + let index = e.currentTarget.id + wx.vibrateShort() + wx.openEmbeddedMiniProgram({ + appId: this.data.link[index].appId, + path: this.data.link[index].linkPage + }); + }, + } +}) diff --git a/node_modules/@jdmini/components/miniprogram_dist/jdwx-link/index.json b/node_modules/@jdmini/components/miniprogram_dist/jdwx-link/index.json new file mode 100644 index 0000000..fba482a --- /dev/null +++ b/node_modules/@jdmini/components/miniprogram_dist/jdwx-link/index.json @@ -0,0 +1,3 @@ +{ + "component": true +} \ No newline at end of file diff --git a/node_modules/@jdmini/components/miniprogram_dist/jdwx-link/index.wxml b/node_modules/@jdmini/components/miniprogram_dist/jdwx-link/index.wxml new file mode 100644 index 0000000..30ff009 --- /dev/null +++ b/node_modules/@jdmini/components/miniprogram_dist/jdwx-link/index.wxml @@ -0,0 +1,11 @@ + + + + {{top.linkName}} + {{top.appDsc}} + + + + {{item.linkName}} + + \ No newline at end of file diff --git a/node_modules/@jdmini/components/miniprogram_dist/jdwx-link/index.wxss b/node_modules/@jdmini/components/miniprogram_dist/jdwx-link/index.wxss new file mode 100644 index 0000000..111b9f1 --- /dev/null +++ b/node_modules/@jdmini/components/miniprogram_dist/jdwx-link/index.wxss @@ -0,0 +1,63 @@ +/* 页面容器 */ +.jdwx-link-component { + /* background-image: linear-gradient(to right, #4F9863, #4F9863); */ + background-attachment: fixed; + background-size: cover; /* 确保背景图像覆盖整个元素 */ + /* height: 100vh; */ + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + padding: 20rpx; + box-sizing: border-box; + overflow: auto; /* 允许内容滚动 */ +} + +/* 列表项样式 */ +.jdwx-applink-list { + display: flex; + align-items: center; + width: 95%; + /* 假设我们想要每个view的高度约为屏幕高度的1/8 */ + /* 使用小程序的wx.getSystemInfo API动态计算并设置这个值会更准确 */ + height: calc((100vh - 40rpx) / 10); /* 减去容器padding的影响 */ + padding: 20rpx; + background-color: rgba(248, 250, 252, 1); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + border-radius: 8rpx; + margin-bottom: 10rpx; +} +.jdwx-applink-top { + display: flex; + align-items: center; + width: 95%; + /* 假设我们想要每个view的高度约为屏幕高度的1/8 */ + /* 使用小程序的wx.getSystemInfo API动态计算并设置这个值会更准确 */ + height: calc((100vh - 40rpx) / 6); /* 减去容器padding的影响 */ + padding: 20rpx; + border-radius: 8rpx; + margin-bottom: 30rpx; + background-color: rgba(248, 250, 252, 1); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} +.jdwx-applink-top-linkname{ + display: flex; + font-size: 36rpx; + color: rgb(39, 37, 37); + padding-bottom: 10rpx; +} +/* 图标样式 */ +.jdwx-applink-icon { + width: 120rpx; + height: 120rpx; + border-radius: 50%; + margin-right: 50rpx; + margin-left: 30rpx; +} + +/* 文本样式 */ +.jdwx-applink-text { + flex: 1; + font-size: 32rpx; + color: rgb(39, 37, 37); +} \ No newline at end of file diff --git a/node_modules/@jdmini/components/package.json b/node_modules/@jdmini/components/package.json new file mode 100644 index 0000000..7d6ed6f --- /dev/null +++ b/node_modules/@jdmini/components/package.json @@ -0,0 +1,20 @@ +{ + "name": "@jdmini/components", + "version": "1.0.6", + "description": "", + "files": [ + "miniprogram_dist", + "resources" + ], + "scripts": { + "pub": "npm publish --access public" + }, + "miniprogram": "miniprogram_dist", + "author": "", + "peerDependencies": { + "@jdmini/api": ">=1.0.8" + }, + "devDependencies": { + "@types/wechat-miniprogram": "^3.4.8" + } +} diff --git a/node_modules/extend/.editorconfig b/node_modules/extend/.editorconfig new file mode 100644 index 0000000..bc228f8 --- /dev/null +++ b/node_modules/extend/.editorconfig @@ -0,0 +1,20 @@ +root = true + +[*] +indent_style = tab +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +max_line_length = 150 + +[CHANGELOG.md] +indent_style = space +indent_size = 2 + +[*.json] +max_line_length = off + +[Makefile] +max_line_length = off diff --git a/node_modules/extend/.eslintrc b/node_modules/extend/.eslintrc new file mode 100644 index 0000000..a34cf28 --- /dev/null +++ b/node_modules/extend/.eslintrc @@ -0,0 +1,17 @@ +{ + "root": true, + + "extends": "@ljharb", + + "rules": { + "complexity": [2, 20], + "eqeqeq": [2, "allow-null"], + "func-name-matching": [1], + "max-depth": [1, 4], + "max-statements": [2, 26], + "no-extra-parens": [1], + "no-magic-numbers": [0], + "no-restricted-syntax": [2, "BreakStatement", "ContinueStatement", "DebuggerStatement", "LabeledStatement", "WithStatement"], + "sort-keys": [0], + } +} diff --git a/node_modules/extend/.jscs.json b/node_modules/extend/.jscs.json new file mode 100644 index 0000000..3cce01d --- /dev/null +++ b/node_modules/extend/.jscs.json @@ -0,0 +1,175 @@ +{ + "es3": true, + + "additionalRules": [], + + "requireSemicolons": true, + + "disallowMultipleSpaces": true, + + "disallowIdentifierNames": [], + + "requireCurlyBraces": { + "allExcept": [], + "keywords": ["if", "else", "for", "while", "do", "try", "catch"] + }, + + "requireSpaceAfterKeywords": ["if", "else", "for", "while", "do", "switch", "return", "try", "catch", "function"], + + "disallowSpaceAfterKeywords": [], + + "disallowSpaceBeforeComma": true, + "disallowSpaceAfterComma": false, + "disallowSpaceBeforeSemicolon": true, + + "disallowNodeTypes": [ + "DebuggerStatement", + "LabeledStatement", + "SwitchCase", + "SwitchStatement", + "WithStatement" + ], + + "requireObjectKeysOnNewLine": { "allExcept": ["sameLine"] }, + + "requireSpacesInAnonymousFunctionExpression": { "beforeOpeningRoundBrace": true, "beforeOpeningCurlyBrace": true }, + "requireSpacesInNamedFunctionExpression": { "beforeOpeningCurlyBrace": true }, + "disallowSpacesInNamedFunctionExpression": { "beforeOpeningRoundBrace": true }, + "requireSpacesInFunctionDeclaration": { "beforeOpeningCurlyBrace": true }, + "disallowSpacesInFunctionDeclaration": { "beforeOpeningRoundBrace": true }, + + "requireSpaceBetweenArguments": true, + + "disallowSpacesInsideParentheses": true, + + "disallowSpacesInsideArrayBrackets": true, + + "disallowQuotedKeysInObjects": { "allExcept": ["reserved"] }, + + "disallowSpaceAfterObjectKeys": true, + + "requireCommaBeforeLineBreak": true, + + "disallowSpaceAfterPrefixUnaryOperators": ["++", "--", "+", "-", "~", "!"], + "requireSpaceAfterPrefixUnaryOperators": [], + + "disallowSpaceBeforePostfixUnaryOperators": ["++", "--"], + "requireSpaceBeforePostfixUnaryOperators": [], + + "disallowSpaceBeforeBinaryOperators": [], + "requireSpaceBeforeBinaryOperators": ["+", "-", "/", "*", "=", "==", "===", "!=", "!=="], + + "requireSpaceAfterBinaryOperators": ["+", "-", "/", "*", "=", "==", "===", "!=", "!=="], + "disallowSpaceAfterBinaryOperators": [], + + "disallowImplicitTypeConversion": ["binary", "string"], + + "disallowKeywords": ["with", "eval"], + + "requireKeywordsOnNewLine": [], + "disallowKeywordsOnNewLine": ["else"], + + "requireLineFeedAtFileEnd": true, + + "disallowTrailingWhitespace": true, + + "disallowTrailingComma": true, + + "excludeFiles": ["node_modules/**", "vendor/**"], + + "disallowMultipleLineStrings": true, + + "requireDotNotation": { "allExcept": ["keywords"] }, + + "requireParenthesesAroundIIFE": true, + + "validateLineBreaks": "LF", + + "validateQuoteMarks": { + "escape": true, + "mark": "'" + }, + + "disallowOperatorBeforeLineBreak": [], + + "requireSpaceBeforeKeywords": [ + "do", + "for", + "if", + "else", + "switch", + "case", + "try", + "catch", + "finally", + "while", + "with", + "return" + ], + + "validateAlignedFunctionParameters": { + "lineBreakAfterOpeningBraces": true, + "lineBreakBeforeClosingBraces": true + }, + + "requirePaddingNewLinesBeforeExport": true, + + "validateNewlineAfterArrayElements": { + "maximum": 6 + }, + + "requirePaddingNewLinesAfterUseStrict": true, + + "disallowArrowFunctions": true, + + "disallowMultiLineTernary": true, + + "validateOrderInObjectKeys": false, + + "disallowIdenticalDestructuringNames": true, + + "disallowNestedTernaries": { "maxLevel": 1 }, + + "requireSpaceAfterComma": { "allExcept": ["trailing"] }, + "requireAlignedMultilineParams": false, + + "requireSpacesInGenerator": { + "afterStar": true + }, + + "disallowSpacesInGenerator": { + "beforeStar": true + }, + + "disallowVar": false, + + "requireArrayDestructuring": false, + + "requireEnhancedObjectLiterals": false, + + "requireObjectDestructuring": false, + + "requireEarlyReturn": false, + + "requireCapitalizedConstructorsNew": { + "allExcept": ["Function", "String", "Object", "Symbol", "Number", "Date", "RegExp", "Error", "Boolean", "Array"] + }, + + "requireImportAlphabetized": false, + + "requireSpaceBeforeObjectValues": true, + "requireSpaceBeforeDestructuredValues": true, + + "disallowSpacesInsideTemplateStringPlaceholders": true, + + "disallowArrayDestructuringReturn": false, + + "requireNewlineBeforeSingleStatementsInIf": false, + + "disallowUnusedVariables": true, + + "requireSpacesInsideImportedObjectBraces": true, + + "requireUseStrict": true +} + diff --git a/node_modules/extend/.travis.yml b/node_modules/extend/.travis.yml new file mode 100644 index 0000000..5ccdfc4 --- /dev/null +++ b/node_modules/extend/.travis.yml @@ -0,0 +1,230 @@ +language: node_js +os: + - linux +node_js: + - "10.7" + - "9.11" + - "8.11" + - "7.10" + - "6.14" + - "5.12" + - "4.9" + - "iojs-v3.3" + - "iojs-v2.5" + - "iojs-v1.8" + - "0.12" + - "0.10" + - "0.8" +before_install: + - 'case "${TRAVIS_NODE_VERSION}" in 0.*) export NPM_CONFIG_STRICT_SSL=false ;; esac' + - 'nvm install-latest-npm' +install: + - 'if [ "${TRAVIS_NODE_VERSION}" = "0.6" ] || [ "${TRAVIS_NODE_VERSION}" = "0.9" ]; then nvm install --latest-npm 0.8 && npm install && nvm use "${TRAVIS_NODE_VERSION}"; else npm install; fi;' +script: + - 'if [ -n "${PRETEST-}" ]; then npm run pretest ; fi' + - 'if [ -n "${POSTTEST-}" ]; then npm run posttest ; fi' + - 'if [ -n "${COVERAGE-}" ]; then npm run coverage ; fi' + - 'if [ -n "${TEST-}" ]; then npm run tests-only ; fi' +sudo: false +env: + - TEST=true +matrix: + fast_finish: true + include: + - node_js: "lts/*" + env: PRETEST=true + - node_js: "lts/*" + env: POSTTEST=true + - node_js: "4" + env: COVERAGE=true + - node_js: "10.6" + env: TEST=true ALLOW_FAILURE=true + - node_js: "10.5" + env: TEST=true ALLOW_FAILURE=true + - node_js: "10.4" + env: TEST=true ALLOW_FAILURE=true + - node_js: "10.3" + env: TEST=true ALLOW_FAILURE=true + - node_js: "10.2" + env: TEST=true ALLOW_FAILURE=true + - node_js: "10.1" + env: TEST=true ALLOW_FAILURE=true + - node_js: "10.0" + env: TEST=true ALLOW_FAILURE=true + - node_js: "9.10" + env: TEST=true ALLOW_FAILURE=true + - node_js: "9.9" + env: TEST=true ALLOW_FAILURE=true + - node_js: "9.8" + env: TEST=true ALLOW_FAILURE=true + - node_js: "9.7" + env: TEST=true ALLOW_FAILURE=true + - node_js: "9.6" + env: TEST=true ALLOW_FAILURE=true + - node_js: "9.5" + env: TEST=true ALLOW_FAILURE=true + - node_js: "9.4" + env: TEST=true ALLOW_FAILURE=true + - node_js: "9.3" + env: TEST=true ALLOW_FAILURE=true + - node_js: "9.2" + env: TEST=true ALLOW_FAILURE=true + - node_js: "9.1" + env: TEST=true ALLOW_FAILURE=true + - node_js: "9.0" + env: TEST=true ALLOW_FAILURE=true + - node_js: "8.10" + env: TEST=true ALLOW_FAILURE=true + - node_js: "8.9" + env: TEST=true ALLOW_FAILURE=true + - node_js: "8.8" + env: TEST=true ALLOW_FAILURE=true + - node_js: "8.7" + env: TEST=true ALLOW_FAILURE=true + - node_js: "8.6" + env: TEST=true ALLOW_FAILURE=true + - node_js: "8.5" + env: TEST=true ALLOW_FAILURE=true + - node_js: "8.4" + env: TEST=true ALLOW_FAILURE=true + - node_js: "8.3" + env: TEST=true ALLOW_FAILURE=true + - node_js: "8.2" + env: TEST=true ALLOW_FAILURE=true + - node_js: "8.1" + env: TEST=true ALLOW_FAILURE=true + - node_js: "8.0" + env: TEST=true ALLOW_FAILURE=true + - node_js: "7.9" + env: TEST=true ALLOW_FAILURE=true + - node_js: "7.8" + env: TEST=true ALLOW_FAILURE=true + - node_js: "7.7" + env: TEST=true ALLOW_FAILURE=true + - node_js: "7.6" + env: TEST=true ALLOW_FAILURE=true + - node_js: "7.5" + env: TEST=true ALLOW_FAILURE=true + - node_js: "7.4" + env: TEST=true ALLOW_FAILURE=true + - node_js: "7.3" + env: TEST=true ALLOW_FAILURE=true + - node_js: "7.2" + env: TEST=true ALLOW_FAILURE=true + - node_js: "7.1" + env: TEST=true ALLOW_FAILURE=true + - node_js: "7.0" + env: TEST=true ALLOW_FAILURE=true + - node_js: "6.13" + env: TEST=true ALLOW_FAILURE=true + - node_js: "6.12" + env: TEST=true ALLOW_FAILURE=true + - node_js: "6.11" + env: TEST=true ALLOW_FAILURE=true + - node_js: "6.10" + env: TEST=true ALLOW_FAILURE=true + - node_js: "6.9" + env: TEST=true ALLOW_FAILURE=true + - node_js: "6.8" + env: TEST=true ALLOW_FAILURE=true + - node_js: "6.7" + env: TEST=true ALLOW_FAILURE=true + - node_js: "6.6" + env: TEST=true ALLOW_FAILURE=true + - node_js: "6.5" + env: TEST=true ALLOW_FAILURE=true + - node_js: "6.4" + env: TEST=true ALLOW_FAILURE=true + - node_js: "6.3" + env: TEST=true ALLOW_FAILURE=true + - node_js: "6.2" + env: TEST=true ALLOW_FAILURE=true + - node_js: "6.1" + env: TEST=true ALLOW_FAILURE=true + - node_js: "6.0" + env: TEST=true ALLOW_FAILURE=true + - node_js: "5.11" + env: TEST=true ALLOW_FAILURE=true + - node_js: "5.10" + env: TEST=true ALLOW_FAILURE=true + - node_js: "5.9" + env: TEST=true ALLOW_FAILURE=true + - node_js: "5.8" + env: TEST=true ALLOW_FAILURE=true + - node_js: "5.7" + env: TEST=true ALLOW_FAILURE=true + - node_js: "5.6" + env: TEST=true ALLOW_FAILURE=true + - node_js: "5.5" + env: TEST=true ALLOW_FAILURE=true + - node_js: "5.4" + env: TEST=true ALLOW_FAILURE=true + - node_js: "5.3" + env: TEST=true ALLOW_FAILURE=true + - node_js: "5.2" + env: TEST=true ALLOW_FAILURE=true + - node_js: "5.1" + env: TEST=true ALLOW_FAILURE=true + - node_js: "5.0" + env: TEST=true ALLOW_FAILURE=true + - node_js: "4.8" + env: TEST=true ALLOW_FAILURE=true + - node_js: "4.7" + env: TEST=true ALLOW_FAILURE=true + - node_js: "4.6" + env: TEST=true ALLOW_FAILURE=true + - node_js: "4.5" + env: TEST=true ALLOW_FAILURE=true + - node_js: "4.4" + env: TEST=true ALLOW_FAILURE=true + - node_js: "4.3" + env: TEST=true ALLOW_FAILURE=true + - node_js: "4.2" + env: TEST=true ALLOW_FAILURE=true + - node_js: "4.1" + env: TEST=true ALLOW_FAILURE=true + - node_js: "4.0" + env: TEST=true ALLOW_FAILURE=true + - node_js: "iojs-v3.2" + env: TEST=true ALLOW_FAILURE=true + - node_js: "iojs-v3.1" + env: TEST=true ALLOW_FAILURE=true + - node_js: "iojs-v3.0" + env: TEST=true ALLOW_FAILURE=true + - node_js: "iojs-v2.4" + env: TEST=true ALLOW_FAILURE=true + - node_js: "iojs-v2.3" + env: TEST=true ALLOW_FAILURE=true + - node_js: "iojs-v2.2" + env: TEST=true ALLOW_FAILURE=true + - node_js: "iojs-v2.1" + env: TEST=true ALLOW_FAILURE=true + - node_js: "iojs-v2.0" + env: TEST=true ALLOW_FAILURE=true + - node_js: "iojs-v1.7" + env: TEST=true ALLOW_FAILURE=true + - node_js: "iojs-v1.6" + env: TEST=true ALLOW_FAILURE=true + - node_js: "iojs-v1.5" + env: TEST=true ALLOW_FAILURE=true + - node_js: "iojs-v1.4" + env: TEST=true ALLOW_FAILURE=true + - node_js: "iojs-v1.3" + env: TEST=true ALLOW_FAILURE=true + - node_js: "iojs-v1.2" + env: TEST=true ALLOW_FAILURE=true + - node_js: "iojs-v1.1" + env: TEST=true ALLOW_FAILURE=true + - node_js: "iojs-v1.0" + env: TEST=true ALLOW_FAILURE=true + - node_js: "0.11" + env: TEST=true ALLOW_FAILURE=true + - node_js: "0.9" + env: TEST=true ALLOW_FAILURE=true + - node_js: "0.6" + env: TEST=true ALLOW_FAILURE=true + - node_js: "0.4" + env: TEST=true ALLOW_FAILURE=true + allow_failures: + - os: osx + - env: TEST=true ALLOW_FAILURE=true diff --git a/node_modules/extend/CHANGELOG.md b/node_modules/extend/CHANGELOG.md new file mode 100644 index 0000000..2cf7de6 --- /dev/null +++ b/node_modules/extend/CHANGELOG.md @@ -0,0 +1,83 @@ +3.0.2 / 2018-07-19 +================== + * [Fix] Prevent merging `__proto__` property (#48) + * [Dev Deps] update `eslint`, `@ljharb/eslint-config`, `tape` + * [Tests] up to `node` `v10.7`, `v9.11`, `v8.11`, `v7.10`, `v6.14`, `v4.9`; use `nvm install-latest-npm` + +3.0.1 / 2017-04-27 +================== + * [Fix] deep extending should work with a non-object (#46) + * [Dev Deps] update `tape`, `eslint`, `@ljharb/eslint-config` + * [Tests] up to `node` `v7.9`, `v6.10`, `v4.8`; improve matrix + * [Docs] Switch from vb.teelaun.ch to versionbadg.es for the npm version badge SVG. + * [Docs] Add example to readme (#34) + +3.0.0 / 2015-07-01 +================== + * [Possible breaking change] Use global "strict" directive (#32) + * [Tests] `int` is an ES3 reserved word + * [Tests] Test up to `io.js` `v2.3` + * [Tests] Add `npm run eslint` + * [Dev Deps] Update `covert`, `jscs` + +2.0.1 / 2015-04-25 +================== + * Use an inline `isArray` check, for ES3 browsers. (#27) + * Some old browsers fail when an identifier is `toString` + * Test latest `node` and `io.js` versions on `travis-ci`; speed up builds + * Add license info to package.json (#25) + * Update `tape`, `jscs` + * Adding a CHANGELOG + +2.0.0 / 2014-10-01 +================== + * Increase code coverage to 100%; run code coverage as part of tests + * Add `npm run lint`; Run linter as part of tests + * Remove nodeType and setInterval checks in isPlainObject + * Updating `tape`, `jscs`, `covert` + * General style and README cleanup + +1.3.0 / 2014-06-20 +================== + * Add component.json for browser support (#18) + * Use SVG for badges in README (#16) + * Updating `tape`, `covert` + * Updating travis-ci to work with multiple node versions + * Fix `deep === false` bug (returning target as {}) (#14) + * Fixing constructor checks in isPlainObject + * Adding additional test coverage + * Adding `npm run coverage` + * Add LICENSE (#13) + * Adding a warning about `false`, per #11 + * General style and whitespace cleanup + +1.2.1 / 2013-09-14 +================== + * Fixing hasOwnProperty bugs that would only have shown up in specific browsers. Fixes #8 + * Updating `tape` + +1.2.0 / 2013-09-02 +================== + * Updating the README: add badges + * Adding a missing variable reference. + * Using `tape` instead of `buster` for tests; add more tests (#7) + * Adding node 0.10 to Travis CI (#6) + * Enabling "npm test" and cleaning up package.json (#5) + * Add Travis CI. + +1.1.3 / 2012-12-06 +================== + * Added unit tests. + * Ensure extend function is named. (Looks nicer in a stack trace.) + * README cleanup. + +1.1.1 / 2012-11-07 +================== + * README cleanup. + * Added installation instructions. + * Added a missing semicolon + +1.0.0 / 2012-04-08 +================== + * Initial commit + diff --git a/node_modules/extend/LICENSE b/node_modules/extend/LICENSE new file mode 100644 index 0000000..e16d6a5 --- /dev/null +++ b/node_modules/extend/LICENSE @@ -0,0 +1,23 @@ +The MIT License (MIT) + +Copyright (c) 2014 Stefan Thomas + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, 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. + diff --git a/node_modules/extend/README.md b/node_modules/extend/README.md new file mode 100644 index 0000000..5b8249a --- /dev/null +++ b/node_modules/extend/README.md @@ -0,0 +1,81 @@ +[![Build Status][travis-svg]][travis-url] +[![dependency status][deps-svg]][deps-url] +[![dev dependency status][dev-deps-svg]][dev-deps-url] + +# extend() for Node.js [![Version Badge][npm-version-png]][npm-url] + +`node-extend` is a port of the classic extend() method from jQuery. It behaves as you expect. It is simple, tried and true. + +Notes: + +* Since Node.js >= 4, + [`Object.assign`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) + now offers the same functionality natively (but without the "deep copy" option). + See [ECMAScript 2015 (ES6) in Node.js](https://nodejs.org/en/docs/es6). +* Some native implementations of `Object.assign` in both Node.js and many + browsers (since NPM modules are for the browser too) may not be fully + spec-compliant. + Check [`object.assign`](https://www.npmjs.com/package/object.assign) module for + a compliant candidate. + +## Installation + +This package is available on [npm][npm-url] as: `extend` + +``` sh +npm install extend +``` + +## Usage + +**Syntax:** extend **(** [`deep`], `target`, `object1`, [`objectN`] **)** + +*Extend one object with one or more others, returning the modified object.* + +**Example:** + +``` js +var extend = require('extend'); +extend(targetObject, object1, object2); +``` + +Keep in mind that the target object will be modified, and will be returned from extend(). + +If a boolean true is specified as the first argument, extend performs a deep copy, recursively copying any objects it finds. Otherwise, the copy will share structure with the original object(s). +Undefined properties are not copied. However, properties inherited from the object's prototype will be copied over. +Warning: passing `false` as the first argument is not supported. + +### Arguments + +* `deep` *Boolean* (optional) +If set, the merge becomes recursive (i.e. deep copy). +* `target` *Object* +The object to extend. +* `object1` *Object* +The object that will be merged into the first. +* `objectN` *Object* (Optional) +More objects to merge into the first. + +## License + +`node-extend` is licensed under the [MIT License][mit-license-url]. + +## Acknowledgements + +All credit to the jQuery authors for perfecting this amazing utility. + +Ported to Node.js by [Stefan Thomas][github-justmoon] with contributions by [Jonathan Buchanan][github-insin] and [Jordan Harband][github-ljharb]. + +[travis-svg]: https://travis-ci.org/justmoon/node-extend.svg +[travis-url]: https://travis-ci.org/justmoon/node-extend +[npm-url]: https://npmjs.org/package/extend +[mit-license-url]: http://opensource.org/licenses/MIT +[github-justmoon]: https://github.com/justmoon +[github-insin]: https://github.com/insin +[github-ljharb]: https://github.com/ljharb +[npm-version-png]: http://versionbadg.es/justmoon/node-extend.svg +[deps-svg]: https://david-dm.org/justmoon/node-extend.svg +[deps-url]: https://david-dm.org/justmoon/node-extend +[dev-deps-svg]: https://david-dm.org/justmoon/node-extend/dev-status.svg +[dev-deps-url]: https://david-dm.org/justmoon/node-extend#info=devDependencies + diff --git a/node_modules/extend/component.json b/node_modules/extend/component.json new file mode 100644 index 0000000..1500a2f --- /dev/null +++ b/node_modules/extend/component.json @@ -0,0 +1,32 @@ +{ + "name": "extend", + "author": "Stefan Thomas (http://www.justmoon.net)", + "version": "3.0.0", + "description": "Port of jQuery.extend for node.js and the browser.", + "scripts": [ + "index.js" + ], + "contributors": [ + { + "name": "Jordan Harband", + "url": "https://github.com/ljharb" + } + ], + "keywords": [ + "extend", + "clone", + "merge" + ], + "repository" : { + "type": "git", + "url": "https://github.com/justmoon/node-extend.git" + }, + "dependencies": { + }, + "devDependencies": { + "tape" : "~3.0.0", + "covert": "~0.4.0", + "jscs": "~1.6.2" + } +} + diff --git a/node_modules/extend/index.js b/node_modules/extend/index.js new file mode 100644 index 0000000..2aa3faa --- /dev/null +++ b/node_modules/extend/index.js @@ -0,0 +1,117 @@ +'use strict'; + +var hasOwn = Object.prototype.hasOwnProperty; +var toStr = Object.prototype.toString; +var defineProperty = Object.defineProperty; +var gOPD = Object.getOwnPropertyDescriptor; + +var isArray = function isArray(arr) { + if (typeof Array.isArray === 'function') { + return Array.isArray(arr); + } + + return toStr.call(arr) === '[object Array]'; +}; + +var isPlainObject = function isPlainObject(obj) { + if (!obj || toStr.call(obj) !== '[object Object]') { + return false; + } + + var hasOwnConstructor = hasOwn.call(obj, 'constructor'); + var hasIsPrototypeOf = obj.constructor && obj.constructor.prototype && hasOwn.call(obj.constructor.prototype, 'isPrototypeOf'); + // Not own constructor property must be Object + if (obj.constructor && !hasOwnConstructor && !hasIsPrototypeOf) { + return false; + } + + // Own properties are enumerated firstly, so to speed up, + // if last one is own, then all properties are own. + var key; + for (key in obj) { /**/ } + + return typeof key === 'undefined' || hasOwn.call(obj, key); +}; + +// If name is '__proto__', and Object.defineProperty is available, define __proto__ as an own property on target +var setProperty = function setProperty(target, options) { + if (defineProperty && options.name === '__proto__') { + defineProperty(target, options.name, { + enumerable: true, + configurable: true, + value: options.newValue, + writable: true + }); + } else { + target[options.name] = options.newValue; + } +}; + +// Return undefined instead of __proto__ if '__proto__' is not an own property +var getProperty = function getProperty(obj, name) { + if (name === '__proto__') { + if (!hasOwn.call(obj, name)) { + return void 0; + } else if (gOPD) { + // In early versions of node, obj['__proto__'] is buggy when obj has + // __proto__ as an own property. Object.getOwnPropertyDescriptor() works. + return gOPD(obj, name).value; + } + } + + return obj[name]; +}; + +module.exports = function extend() { + var options, name, src, copy, copyIsArray, clone; + var target = arguments[0]; + var i = 1; + var length = arguments.length; + var deep = false; + + // Handle a deep copy situation + if (typeof target === 'boolean') { + deep = target; + target = arguments[1] || {}; + // skip the boolean and the target + i = 2; + } + if (target == null || (typeof target !== 'object' && typeof target !== 'function')) { + target = {}; + } + + for (; i < length; ++i) { + options = arguments[i]; + // Only deal with non-null/undefined values + if (options != null) { + // Extend the base object + for (name in options) { + src = getProperty(target, name); + copy = getProperty(options, name); + + // Prevent never-ending loop + if (target !== copy) { + // Recurse if we're merging plain objects or arrays + if (deep && copy && (isPlainObject(copy) || (copyIsArray = isArray(copy)))) { + if (copyIsArray) { + copyIsArray = false; + clone = src && isArray(src) ? src : []; + } else { + clone = src && isPlainObject(src) ? src : {}; + } + + // Never move original objects, clone them + setProperty(target, { name: name, newValue: extend(deep, clone, copy) }); + + // Don't bring in undefined values + } else if (typeof copy !== 'undefined') { + setProperty(target, { name: name, newValue: copy }); + } + } + } + } + } + + // Return the modified object + return target; +}; diff --git a/node_modules/extend/package.json b/node_modules/extend/package.json new file mode 100644 index 0000000..85279f7 --- /dev/null +++ b/node_modules/extend/package.json @@ -0,0 +1,42 @@ +{ + "name": "extend", + "author": "Stefan Thomas (http://www.justmoon.net)", + "version": "3.0.2", + "description": "Port of jQuery.extend for node.js and the browser", + "main": "index", + "scripts": { + "pretest": "npm run lint", + "test": "npm run tests-only", + "posttest": "npm run coverage-quiet", + "tests-only": "node test", + "coverage": "covert test/index.js", + "coverage-quiet": "covert test/index.js --quiet", + "lint": "npm run jscs && npm run eslint", + "jscs": "jscs *.js */*.js", + "eslint": "eslint *.js */*.js" + }, + "contributors": [ + { + "name": "Jordan Harband", + "url": "https://github.com/ljharb" + } + ], + "keywords": [ + "extend", + "clone", + "merge" + ], + "repository": { + "type": "git", + "url": "https://github.com/justmoon/node-extend.git" + }, + "dependencies": {}, + "devDependencies": { + "@ljharb/eslint-config": "^12.2.1", + "covert": "^1.1.0", + "eslint": "^4.19.1", + "jscs": "^3.0.7", + "tape": "^4.9.1" + }, + "license": "MIT" +} diff --git a/node_modules/weapp-qrcode/.editorconfig b/node_modules/weapp-qrcode/.editorconfig new file mode 100644 index 0000000..9d08a1a --- /dev/null +++ b/node_modules/weapp-qrcode/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/node_modules/weapp-qrcode/.eslintignore b/node_modules/weapp-qrcode/.eslintignore new file mode 100644 index 0000000..e0b9f74 --- /dev/null +++ b/node_modules/weapp-qrcode/.eslintignore @@ -0,0 +1,2 @@ +build/*.js +src/qrcode.js diff --git a/node_modules/weapp-qrcode/.eslintrc.js b/node_modules/weapp-qrcode/.eslintrc.js new file mode 100644 index 0000000..6913aee --- /dev/null +++ b/node_modules/weapp-qrcode/.eslintrc.js @@ -0,0 +1,26 @@ +module.exports = { + root: true, + parser: 'babel-eslint', + parserOptions: { + sourceType: 'module' + }, + env: { + browser: true, + }, + // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style + extends: 'standard', + "globals": { + __VERSION__: false, + ENV: false, + wx: false + }, + // add your custom rules here + 'rules': { + // allow paren-less arrow functions + 'arrow-parens': 0, + // allow async-await + 'generator-star-spacing': 0, + // allow debugger during development + 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 + } +} diff --git a/node_modules/weapp-qrcode/.github/ISSUE_TEMPLATE.md b/node_modules/weapp-qrcode/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..3df4aac --- /dev/null +++ b/node_modules/weapp-qrcode/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,37 @@ +# Description + +[问题简单描述] + +# Environment + +* Platform: [开发者工具/iOS/Andriod/Web] +* Platform version: [对应工具或者iOS或者Andriod的版本号] +* Wechat version: [微信版本号] +* weapp-qrcode version: [在package.json里] +* other version: [如果在某一设备下出现该问题,请填写设备号] + +# Reproduce + +**问题复现步骤:** + +1. [第一步] +2. [第二步] +3. [其他步骤...] + +**期望的表现:** + +[在这里描述期望的表现] + +**观察到的表现:** + +[在这里描述观察到的表现] + +**屏幕截图和动态 GIF 图** + +![复现步骤的屏幕截图和动态 GIF 图](图片的 url) + +# Relevant Code / Logs + +``` +// TODO(you): code or logs here to reproduce the problem +``` diff --git a/node_modules/weapp-qrcode/.travis.yml b/node_modules/weapp-qrcode/.travis.yml new file mode 100644 index 0000000..8e7f7fa --- /dev/null +++ b/node_modules/weapp-qrcode/.travis.yml @@ -0,0 +1,23 @@ +language: node_js +sudo: required +node_js: + - 10.0.0 +cache: + directories: + - node_modules +before_install: + - export TZ='Asia/Shanghai' +install: + - npm install +script: + - npm run publish + +after_script: + - git init + - git config user.name "${USER_NAME}" + - git config user.email "${USER_EMAIL}" + - git add . + - git commit -m "publish" + - git push -f https://${access_token}@github.com/yingye/weapp-qrcode HEAD:master + +branch: master \ No newline at end of file diff --git a/node_modules/weapp-qrcode/CHANGELOG.md b/node_modules/weapp-qrcode/CHANGELOG.md new file mode 100644 index 0000000..7285d89 --- /dev/null +++ b/node_modules/weapp-qrcode/CHANGELOG.md @@ -0,0 +1,40 @@ +# Change Log + +Change log for weapp-qrcode. [Details at Github](https://github.com/yingye/weapp-qrcode) + +## [1.0.0] - 2018-12-25 + +- 支持传入绘图上下文(CanvasContext). + +## [1.0.0-beta] - 2018-12-13 + +- 支持二维码在 canvas 上绘制的起始位置. +- 支持在二维码上绘制图片及绘制位置. + +## [0.9.0] - 2018-05-31 + +- 支持绘制带中文的二维码. + +## [0.8.0] - 2018-05-15 + +- 绘制二维码后添加回调函数. + +## [0.7.0] - 2018-05-11 + +- 支持在小程序组件中绘制二维码. + +## [0.6.0] - 2018-04-16 + +- Add multi-output. + +## [0.5.0] - 2018-03-11 + +- Add version in weapp.qrcode.js. + +## [0.4.0] - 2018-03-10 + +- Fix options. + +## [0.3.0] - 2018-02-03 + +- Initial release. diff --git a/node_modules/weapp-qrcode/LICENSE b/node_modules/weapp-qrcode/LICENSE new file mode 100644 index 0000000..1ae54aa --- /dev/null +++ b/node_modules/weapp-qrcode/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 yingye + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +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. diff --git a/node_modules/weapp-qrcode/README.md b/node_modules/weapp-qrcode/README.md new file mode 100644 index 0000000..4fe7896 --- /dev/null +++ b/node_modules/weapp-qrcode/README.md @@ -0,0 +1,96 @@ +# weapp-qrcode + +[![npm version](https://badge.fury.io/js/weapp-qrcode.svg)](https://badge.fury.io/js/weapp-qrcode) +[![change-log](https://img.shields.io/badge/changelog-md-blue.svg)](https://github.com/yingye/weapp-qrcode/blob/master/CHANGELOG.md) + +weapp.qrcode.js 在 微信小程序 中,快速生成二维码 + +## Usage + +先在 wxml 文件中,创建绘制的 `canvas`,并定义好 `width`, `height`, `canvasId` 。 + +```html + +``` + +直接引入 js 文件,使用 `drawQrcode()` 绘制二维码。!!!在 调用 `drawQrcode()` 方法之前,一定要确保可以获取到 `canvas context` 。 + +在 v0.6.0 版本构建出多个文件,详情移步[Build Files说明](https://github.com/yingye/weapp-qrcode/blob/master/dist/README.md)。 + +```js +// 将 dist 目录下,weapp.qrcode.esm.js 复制到项目目录中 +import drawQrcode from '../../utils/weapp.qrcode.esm.js' + +drawQrcode({ + width: 200, + height: 200, + canvasId: 'myQrcode', + // ctx: wx.createCanvasContext('myQrcode'), + text: 'https://github.com/yingye', + // v1.0.0+版本支持在二维码上绘制图片 + image: { + imageResource: '../../images/icon.png', + dx: 70, + dy: 70, + dWidth: 60, + dHeight: 60 + } +}) +``` + +如果项目使用了 wepy 框架,可直接安装 `weapp-qrcode` npm包。 + +``` +npm install weapp-qrcode --save +``` + +```js +import drawQrcode from 'weapp-qrcode' + +drawQrcode({ + width: 200, + height: 200, + canvasId: 'myQrcode', + text: 'https://github.com/yingye' +}) +``` + +## DEMO + + + +更多 demo 可以参考 [examples目录](https://github.com/yingye/weapp-qrcode/tree/master/examples),示例包含原生语法及WePY、mpvue、Taro框架。 + +## API + +### drawQrcode([options]) + +#### options + +Type: Object + +| 参数 | 说明 | 示例| +| ------ | ------ | ------ | +| width | 必须,二维码宽度,与`canvas`的`width`保持一致 | 200 | +| height | 必须,二维码高度,与`canvas`的`height`保持一致 | 200 | +| canvasId | 非必须,绘制的`canvasId` | `'myQrcode'` | +| ctx | 非必须,绘图上下文,可通过 `wx.createCanvasContext('canvasId')` 获取,v1.0.0+版本支持 | `'wx.createCanvasContext('canvasId')'` | +| text | 必须,二维码内容 | 'https://github.com/yingye' | +| typeNumber | 非必须,二维码的计算模式,默认值-1 | 8 | +| correctLevel | 非必须,二维码纠错级别,默认值为高级,取值:`{ L: 1, M: 0, Q: 3, H: 2 }` | 1 | +| background | 非必须,二维码背景颜色,默认值白色 | `'#ffffff'` | +| foreground | 非必须,二维码前景色,默认值黑色 | `'#000000'` | +| _this | 非必须,若在组件中使用,需要传入,v0.7.0+版本支持 | this | +| callback | 非必须,绘制完成后的回调函数,v0.8.0+版本支持。安卓手机兼容性问题,可通过自行设置计时器来解决,更多可以参考 [issue #18](https://github.com/yingye/weapp-qrcode/issues/18) | `function (e) { console.log('e', e) }` | +| x | 非必须,二维码绘制的 x 轴起始位置,默认值0,v1.0.0+版本支持 | 100 | +| y | 非必须,二维码绘制的 y 轴起始位置,默认值0,v1.0.0+版本支持 | 100 | +| image | 非必须,在 canvas 上绘制图片,**层级高于二维码**,v1.0.0+版本支持,更多可参考[drawImage](https://developers.weixin.qq.com/miniprogram/dev/api/CanvasContext.drawImage.html) | `{ imageResource: '', dx: 0, dy: 0, dWidth: 100, dHeight: 100 }` | + + +**位置信息可以参见下图:** + + + +## TIPS + +weapp.qrcode.js 二维码生成部分借鉴了 jquery-qrcode 源码,可以参考 [jquery-qrcode](https://github.com/jeromeetienne/jquery-qrcode)。 diff --git a/node_modules/weapp-qrcode/build/rollup.dev.config.js b/node_modules/weapp-qrcode/build/rollup.dev.config.js new file mode 100644 index 0000000..a8ef569 --- /dev/null +++ b/node_modules/weapp-qrcode/build/rollup.dev.config.js @@ -0,0 +1,39 @@ +var babel = require('rollup-plugin-babel') +var resolve = require('rollup-plugin-node-resolve') +var commonjs = require('rollup-plugin-commonjs') +var eslint = require('rollup-plugin-eslint') +var license = require('rollup-plugin-license') + +var path = require('path') + +var pkg = require('../package.json') + +module.exports = { + input: path.resolve(__dirname, '../src/index.js'), + output: [ + { + file: path.resolve(__dirname, '../dist/weapp.qrcode.js'), + format: 'umd' + }, + { + file: path.resolve(__dirname, '../examples/wechat-app/utils/weapp.qrcode.js'), + format: 'umd' + } + ], + moduleName: 'drawQrcode', + plugins: [ + eslint(), + resolve({ + jsnext: true, + main: true, + browser: true + }), + commonjs(), + babel({ + exclude: 'node_modules/**' + }), + license({ + banner: 'weapp.qrcode.js v' + pkg.version + ' (' + pkg.homepage + ')' + }) + ] +} \ No newline at end of file diff --git a/node_modules/weapp-qrcode/build/rollup.prod.config.js b/node_modules/weapp-qrcode/build/rollup.prod.config.js new file mode 100644 index 0000000..c00e826 --- /dev/null +++ b/node_modules/weapp-qrcode/build/rollup.prod.config.js @@ -0,0 +1,49 @@ +var babel = require('rollup-plugin-babel') +var babel = require('rollup-plugin-babel') +var resolve = require('rollup-plugin-node-resolve') +var commonjs = require('rollup-plugin-commonjs') +var eslint = require('rollup-plugin-eslint') +var license = require('rollup-plugin-license') +var uglify = require('rollup-plugin-uglify') + +var path = require('path') +var pkg = require('../package.json') + +module.exports = { + input: path.resolve(__dirname, '../src/index.js'), + output: [ + { + file: path.resolve(__dirname, '../dist/weapp.qrcode.min.js'), + format: 'umd' + }, + { + file: path.resolve(__dirname, '../dist/weapp.qrcode.common.js'), + format: 'cjs' + }, + { + file: path.resolve(__dirname, '../dist/weapp.qrcode.esm.js'), + format: 'es' + } + ], + moduleName: 'drawQrcode', + plugins: [ + eslint(), + resolve({ + jsnext: true, + main: true, + browser: true + }), + commonjs(), + babel({ + exclude: 'node_modules/**' + }), + uglify({ + compress: { + // 'drop_console': true + } + }), + license({ + banner: 'weapp.qrcode.js v' + pkg.version + ' (' + pkg.homepage + ')' + }) + ] +} \ No newline at end of file diff --git a/node_modules/weapp-qrcode/dist/README.md b/node_modules/weapp-qrcode/dist/README.md new file mode 100644 index 0000000..5402359 --- /dev/null +++ b/node_modules/weapp-qrcode/dist/README.md @@ -0,0 +1,6 @@ +## Explanation of Build Files + +| | UMD | CommonJS | ES Module | +| --- | --- | --- | --- | +| **Develpment** | weapp.qrcode.js | weapp.qrcode.common.js | weapp.qrcode.esm.js | +| **Production** | weapp.qrcode.min.js | | | diff --git a/node_modules/weapp-qrcode/dist/weapp.qrcode.common.js b/node_modules/weapp-qrcode/dist/weapp.qrcode.common.js new file mode 100644 index 0000000..c3b37c4 --- /dev/null +++ b/node_modules/weapp-qrcode/dist/weapp.qrcode.common.js @@ -0,0 +1,5 @@ +/** + * weapp.qrcode.js v1.0.0 (https://github.com/yingye/weapp-qrcode#readme) + */ + +"use strict";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++}},module.exports=drawQrcode; diff --git a/node_modules/weapp-qrcode/dist/weapp.qrcode.esm.js b/node_modules/weapp-qrcode/dist/weapp.qrcode.esm.js new file mode 100644 index 0000000..5613f25 --- /dev/null +++ b/node_modules/weapp-qrcode/dist/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; diff --git a/node_modules/weapp-qrcode/dist/weapp.qrcode.js b/node_modules/weapp-qrcode/dist/weapp.qrcode.js new file mode 100644 index 0000000..2967226 --- /dev/null +++ b/node_modules/weapp-qrcode/dist/weapp.qrcode.js @@ -0,0 +1,1281 @@ +/** + * weapp.qrcode.js v1.0.0 (https://github.com/yingye/weapp-qrcode#readme) + */ + +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global.drawQrcode = factory()); +}(this, (function () { 'use strict'; + +var hasOwn = Object.prototype.hasOwnProperty; +var toStr = Object.prototype.toString; +var defineProperty = Object.defineProperty; +var gOPD = Object.getOwnPropertyDescriptor; + +var isArray = function isArray(arr) { + if (typeof Array.isArray === 'function') { + return Array.isArray(arr); + } + + return toStr.call(arr) === '[object Array]'; +}; + +var isPlainObject = function isPlainObject(obj) { + if (!obj || toStr.call(obj) !== '[object Object]') { + return false; + } + + var hasOwnConstructor = hasOwn.call(obj, 'constructor'); + var hasIsPrototypeOf = obj.constructor && obj.constructor.prototype && hasOwn.call(obj.constructor.prototype, 'isPrototypeOf'); + // Not own constructor property must be Object + if (obj.constructor && !hasOwnConstructor && !hasIsPrototypeOf) { + return false; + } + + // Own properties are enumerated firstly, so to speed up, + // if last one is own, then all properties are own. + var key; + for (key in obj) { /**/ } + + return typeof key === 'undefined' || hasOwn.call(obj, key); +}; + +// If name is '__proto__', and Object.defineProperty is available, define __proto__ as an own property on target +var setProperty = function setProperty(target, options) { + if (defineProperty && options.name === '__proto__') { + defineProperty(target, options.name, { + enumerable: true, + configurable: true, + value: options.newValue, + writable: true + }); + } else { + target[options.name] = options.newValue; + } +}; + +// Return undefined instead of __proto__ if '__proto__' is not an own property +var getProperty = function getProperty(obj, name) { + if (name === '__proto__') { + if (!hasOwn.call(obj, name)) { + return void 0; + } else if (gOPD) { + // In early versions of node, obj['__proto__'] is buggy when obj has + // __proto__ as an own property. Object.getOwnPropertyDescriptor() works. + return gOPD(obj, name).value; + } + } + + return obj[name]; +}; + +var extend = function extend() { + var options, name, src, copy, copyIsArray, clone; + var target = arguments[0]; + var i = 1; + var length = arguments.length; + var deep = false; + + // Handle a deep copy situation + if (typeof target === 'boolean') { + deep = target; + target = arguments[1] || {}; + // skip the boolean and the target + i = 2; + } + if (target == null || (typeof target !== 'object' && typeof target !== 'function')) { + target = {}; + } + + for (; i < length; ++i) { + options = arguments[i]; + // Only deal with non-null/undefined values + if (options != null) { + // Extend the base object + for (name in options) { + src = getProperty(target, name); + copy = getProperty(options, name); + + // Prevent never-ending loop + if (target !== copy) { + // Recurse if we're merging plain objects or arrays + if (deep && copy && (isPlainObject(copy) || (copyIsArray = isArray(copy)))) { + if (copyIsArray) { + copyIsArray = false; + clone = src && isArray(src) ? src : []; + } else { + clone = src && isPlainObject(src) ? src : {}; + } + + // Never move original objects, clone them + setProperty(target, { name: name, newValue: extend(deep, clone, copy) }); + + // Don't bring in undefined values + } else if (typeof copy !== 'undefined') { + setProperty(target, { name: name, newValue: copy }); + } + } + } + } + } + + // Return the modified object + return target; +}; + +//--------------------------------------------------------------------- +// QRCode for JavaScript +// +// Copyright (c) 2009 Kazuhiko Arase +// +// URL: http://www.d-project.com/ +// +// Licensed under the MIT license: +// http://www.opensource.org/licenses/mit-license.php +// +// The word "QR Code" is registered trademark of +// DENSO WAVE INCORPORATED +// http://www.denso-wave.com/qrcode/faqpatent-e.html +// +//--------------------------------------------------------------------- + +//--------------------------------------------------------------------- +// QR8bitByte +//--------------------------------------------------------------------- + +function QR8bitByte(data) { + this.mode = QRMode.MODE_8BIT_BYTE; + this.data = data; +} + +QR8bitByte.prototype = { + + getLength: function (buffer) { + return this.data.length; + }, + + write: function (buffer) { + for (var i = 0; i < this.data.length; i++) { + // not JIS ... + buffer.put(this.data.charCodeAt(i), 8); + } + } +}; + +//--------------------------------------------------------------------- +// QRCode +//--------------------------------------------------------------------- + +function QRCode(typeNumber, errorCorrectLevel) { + this.typeNumber = typeNumber; + this.errorCorrectLevel = errorCorrectLevel; + this.modules = null; + this.moduleCount = 0; + this.dataCache = null; + this.dataList = new Array(); +} + +QRCode.prototype = { + + addData: function (data) { + var newData = new QR8bitByte(data); + this.dataList.push(newData); + this.dataCache = null; + }, + + isDark: function (row, col) { + if (row < 0 || this.moduleCount <= row || col < 0 || this.moduleCount <= col) { + throw new Error(row + "," + col); + } + return this.modules[row][col]; + }, + + getModuleCount: function () { + return this.moduleCount; + }, + + make: function () { + // Calculate automatically typeNumber if provided is < 1 + if (this.typeNumber < 1) { + var typeNumber = 1; + for (typeNumber = 1; typeNumber < 40; typeNumber++) { + var rsBlocks = QRRSBlock.getRSBlocks(typeNumber, this.errorCorrectLevel); + + var buffer = new QRBitBuffer(); + var totalDataCount = 0; + for (var i = 0; i < rsBlocks.length; i++) { + totalDataCount += rsBlocks[i].dataCount; + } + + for (var i = 0; i < this.dataList.length; i++) { + var data = this.dataList[i]; + buffer.put(data.mode, 4); + buffer.put(data.getLength(), QRUtil.getLengthInBits(data.mode, typeNumber)); + data.write(buffer); + } + if (buffer.getLengthInBits() <= totalDataCount * 8) break; + } + this.typeNumber = typeNumber; + } + this.makeImpl(false, this.getBestMaskPattern()); + }, + + makeImpl: function (test, maskPattern) { + + this.moduleCount = this.typeNumber * 4 + 17; + this.modules = new Array(this.moduleCount); + + for (var row = 0; row < this.moduleCount; row++) { + + this.modules[row] = new Array(this.moduleCount); + + for (var col = 0; col < this.moduleCount; col++) { + this.modules[row][col] = null; //(col + row) % 3; + } + } + + this.setupPositionProbePattern(0, 0); + this.setupPositionProbePattern(this.moduleCount - 7, 0); + this.setupPositionProbePattern(0, this.moduleCount - 7); + this.setupPositionAdjustPattern(); + this.setupTimingPattern(); + this.setupTypeInfo(test, maskPattern); + + if (this.typeNumber >= 7) { + this.setupTypeNumber(test); + } + + if (this.dataCache == null) { + this.dataCache = QRCode.createData(this.typeNumber, this.errorCorrectLevel, this.dataList); + } + + this.mapData(this.dataCache, maskPattern); + }, + + setupPositionProbePattern: function (row, col) { + + for (var r = -1; r <= 7; r++) { + + if (row + r <= -1 || this.moduleCount <= row + r) continue; + + for (var c = -1; c <= 7; c++) { + + if (col + c <= -1 || this.moduleCount <= col + c) continue; + + if (0 <= r && r <= 6 && (c == 0 || c == 6) || 0 <= c && c <= 6 && (r == 0 || r == 6) || 2 <= r && r <= 4 && 2 <= c && c <= 4) { + this.modules[row + r][col + c] = true; + } else { + this.modules[row + r][col + c] = false; + } + } + } + }, + + getBestMaskPattern: function () { + + var minLostPoint = 0; + var pattern = 0; + + for (var i = 0; i < 8; i++) { + + this.makeImpl(true, i); + + var lostPoint = QRUtil.getLostPoint(this); + + if (i == 0 || minLostPoint > lostPoint) { + minLostPoint = lostPoint; + pattern = i; + } + } + + return pattern; + }, + + createMovieClip: function (target_mc, instance_name, depth) { + + var qr_mc = target_mc.createEmptyMovieClip(instance_name, depth); + var cs = 1; + + this.make(); + + for (var row = 0; row < this.modules.length; row++) { + + var y = row * cs; + + for (var col = 0; col < this.modules[row].length; col++) { + + var x = col * cs; + var dark = this.modules[row][col]; + + if (dark) { + qr_mc.beginFill(0, 100); + qr_mc.moveTo(x, y); + qr_mc.lineTo(x + cs, y); + qr_mc.lineTo(x + cs, y + cs); + qr_mc.lineTo(x, y + cs); + qr_mc.endFill(); + } + } + } + + return qr_mc; + }, + + setupTimingPattern: function () { + + for (var r = 8; r < this.moduleCount - 8; r++) { + if (this.modules[r][6] != null) { + continue; + } + this.modules[r][6] = r % 2 == 0; + } + + for (var c = 8; c < this.moduleCount - 8; c++) { + if (this.modules[6][c] != null) { + continue; + } + this.modules[6][c] = c % 2 == 0; + } + }, + + setupPositionAdjustPattern: function () { + + var pos = QRUtil.getPatternPosition(this.typeNumber); + + for (var i = 0; i < pos.length; i++) { + + for (var j = 0; j < pos.length; j++) { + + var row = pos[i]; + var col = pos[j]; + + if (this.modules[row][col] != null) { + continue; + } + + for (var r = -2; r <= 2; r++) { + + for (var c = -2; c <= 2; c++) { + + if (r == -2 || r == 2 || c == -2 || c == 2 || r == 0 && c == 0) { + this.modules[row + r][col + c] = true; + } else { + this.modules[row + r][col + c] = false; + } + } + } + } + } + }, + + setupTypeNumber: function (test) { + + var bits = QRUtil.getBCHTypeNumber(this.typeNumber); + + for (var i = 0; i < 18; i++) { + var mod = !test && (bits >> i & 1) == 1; + this.modules[Math.floor(i / 3)][i % 3 + this.moduleCount - 8 - 3] = mod; + } + + for (var i = 0; i < 18; i++) { + var mod = !test && (bits >> i & 1) == 1; + this.modules[i % 3 + this.moduleCount - 8 - 3][Math.floor(i / 3)] = mod; + } + }, + + setupTypeInfo: function (test, maskPattern) { + + var data = this.errorCorrectLevel << 3 | maskPattern; + var bits = QRUtil.getBCHTypeInfo(data); + + // vertical + for (var i = 0; i < 15; i++) { + + var mod = !test && (bits >> i & 1) == 1; + + if (i < 6) { + this.modules[i][8] = mod; + } else if (i < 8) { + this.modules[i + 1][8] = mod; + } else { + this.modules[this.moduleCount - 15 + i][8] = mod; + } + } + + // horizontal + for (var i = 0; i < 15; i++) { + + var mod = !test && (bits >> i & 1) == 1; + + if (i < 8) { + this.modules[8][this.moduleCount - i - 1] = mod; + } else if (i < 9) { + this.modules[8][15 - i - 1 + 1] = mod; + } else { + this.modules[8][15 - i - 1] = mod; + } + } + + // fixed module + this.modules[this.moduleCount - 8][8] = !test; + }, + + mapData: function (data, maskPattern) { + + var inc = -1; + var row = this.moduleCount - 1; + var bitIndex = 7; + var byteIndex = 0; + + for (var col = this.moduleCount - 1; col > 0; col -= 2) { + + if (col == 6) col--; + + while (true) { + + for (var c = 0; c < 2; c++) { + + if (this.modules[row][col - c] == null) { + + var dark = false; + + if (byteIndex < data.length) { + dark = (data[byteIndex] >>> bitIndex & 1) == 1; + } + + var mask = QRUtil.getMask(maskPattern, row, col - c); + + if (mask) { + dark = !dark; + } + + this.modules[row][col - c] = dark; + bitIndex--; + + if (bitIndex == -1) { + byteIndex++; + bitIndex = 7; + } + } + } + + row += inc; + + if (row < 0 || this.moduleCount <= row) { + row -= inc; + inc = -inc; + break; + } + } + } + } + +}; + +QRCode.PAD0 = 0xEC; +QRCode.PAD1 = 0x11; + +QRCode.createData = function (typeNumber, errorCorrectLevel, dataList) { + + var rsBlocks = QRRSBlock.getRSBlocks(typeNumber, errorCorrectLevel); + + var buffer = new QRBitBuffer(); + + for (var i = 0; i < dataList.length; i++) { + var data = dataList[i]; + buffer.put(data.mode, 4); + buffer.put(data.getLength(), QRUtil.getLengthInBits(data.mode, typeNumber)); + data.write(buffer); + } + + // calc num max data. + var totalDataCount = 0; + for (var i = 0; i < rsBlocks.length; i++) { + totalDataCount += rsBlocks[i].dataCount; + } + + if (buffer.getLengthInBits() > totalDataCount * 8) { + throw new Error("code length overflow. (" + buffer.getLengthInBits() + ">" + totalDataCount * 8 + ")"); + } + + // end code + if (buffer.getLengthInBits() + 4 <= totalDataCount * 8) { + buffer.put(0, 4); + } + + // padding + while (buffer.getLengthInBits() % 8 != 0) { + buffer.putBit(false); + } + + // padding + while (true) { + + if (buffer.getLengthInBits() >= totalDataCount * 8) { + break; + } + buffer.put(QRCode.PAD0, 8); + + if (buffer.getLengthInBits() >= totalDataCount * 8) { + break; + } + buffer.put(QRCode.PAD1, 8); + } + + return QRCode.createBytes(buffer, rsBlocks); +}; + +QRCode.createBytes = function (buffer, rsBlocks) { + + var offset = 0; + + var maxDcCount = 0; + var maxEcCount = 0; + + var dcdata = new Array(rsBlocks.length); + var ecdata = new Array(rsBlocks.length); + + for (var r = 0; r < rsBlocks.length; r++) { + + var dcCount = rsBlocks[r].dataCount; + var ecCount = rsBlocks[r].totalCount - dcCount; + + maxDcCount = Math.max(maxDcCount, dcCount); + maxEcCount = Math.max(maxEcCount, ecCount); + + dcdata[r] = new Array(dcCount); + + for (var i = 0; i < dcdata[r].length; i++) { + dcdata[r][i] = 0xff & buffer.buffer[i + offset]; + } + offset += dcCount; + + var rsPoly = QRUtil.getErrorCorrectPolynomial(ecCount); + var rawPoly = new QRPolynomial(dcdata[r], rsPoly.getLength() - 1); + + var modPoly = rawPoly.mod(rsPoly); + ecdata[r] = new Array(rsPoly.getLength() - 1); + for (var i = 0; i < ecdata[r].length; i++) { + var modIndex = i + modPoly.getLength() - ecdata[r].length; + ecdata[r][i] = modIndex >= 0 ? modPoly.get(modIndex) : 0; + } + } + + var totalCodeCount = 0; + for (var i = 0; i < rsBlocks.length; i++) { + totalCodeCount += rsBlocks[i].totalCount; + } + + var data = new Array(totalCodeCount); + var index = 0; + + for (var i = 0; i < maxDcCount; i++) { + for (var r = 0; r < rsBlocks.length; r++) { + if (i < dcdata[r].length) { + data[index++] = dcdata[r][i]; + } + } + } + + for (var i = 0; i < maxEcCount; i++) { + for (var r = 0; r < rsBlocks.length; r++) { + if (i < ecdata[r].length) { + data[index++] = ecdata[r][i]; + } + } + } + + return data; +}; + +//--------------------------------------------------------------------- +// QRMode +//--------------------------------------------------------------------- + +var QRMode = { + MODE_NUMBER: 1 << 0, + MODE_ALPHA_NUM: 1 << 1, + MODE_8BIT_BYTE: 1 << 2, + MODE_KANJI: 1 << 3 +}; + +//--------------------------------------------------------------------- +// QRErrorCorrectLevel +//--------------------------------------------------------------------- + +var QRErrorCorrectLevel = { + L: 1, + M: 0, + Q: 3, + H: 2 +}; + +//--------------------------------------------------------------------- +// QRMaskPattern +//--------------------------------------------------------------------- + +var QRMaskPattern = { + PATTERN000: 0, + PATTERN001: 1, + PATTERN010: 2, + PATTERN011: 3, + PATTERN100: 4, + PATTERN101: 5, + PATTERN110: 6, + PATTERN111: 7 +}; + +//--------------------------------------------------------------------- +// QRUtil +//--------------------------------------------------------------------- + +var QRUtil = { + + PATTERN_POSITION_TABLE: [[], [6, 18], [6, 22], [6, 26], [6, 30], [6, 34], [6, 22, 38], [6, 24, 42], [6, 26, 46], [6, 28, 50], [6, 30, 54], [6, 32, 58], [6, 34, 62], [6, 26, 46, 66], [6, 26, 48, 70], [6, 26, 50, 74], [6, 30, 54, 78], [6, 30, 56, 82], [6, 30, 58, 86], [6, 34, 62, 90], [6, 28, 50, 72, 94], [6, 26, 50, 74, 98], [6, 30, 54, 78, 102], [6, 28, 54, 80, 106], [6, 32, 58, 84, 110], [6, 30, 58, 86, 114], [6, 34, 62, 90, 118], [6, 26, 50, 74, 98, 122], [6, 30, 54, 78, 102, 126], [6, 26, 52, 78, 104, 130], [6, 30, 56, 82, 108, 134], [6, 34, 60, 86, 112, 138], [6, 30, 58, 86, 114, 142], [6, 34, 62, 90, 118, 146], [6, 30, 54, 78, 102, 126, 150], [6, 24, 50, 76, 102, 128, 154], [6, 28, 54, 80, 106, 132, 158], [6, 32, 58, 84, 110, 136, 162], [6, 26, 54, 82, 110, 138, 166], [6, 30, 58, 86, 114, 142, 170]], + + G15: 1 << 10 | 1 << 8 | 1 << 5 | 1 << 4 | 1 << 2 | 1 << 1 | 1 << 0, + G18: 1 << 12 | 1 << 11 | 1 << 10 | 1 << 9 | 1 << 8 | 1 << 5 | 1 << 2 | 1 << 0, + G15_MASK: 1 << 14 | 1 << 12 | 1 << 10 | 1 << 4 | 1 << 1, + + getBCHTypeInfo: function (data) { + var d = data << 10; + while (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G15) >= 0) { + d ^= QRUtil.G15 << QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G15); + } + return (data << 10 | d) ^ QRUtil.G15_MASK; + }, + + getBCHTypeNumber: function (data) { + var d = data << 12; + while (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G18) >= 0) { + d ^= QRUtil.G18 << QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G18); + } + return data << 12 | d; + }, + + getBCHDigit: function (data) { + + var digit = 0; + + while (data != 0) { + digit++; + data >>>= 1; + } + + return digit; + }, + + getPatternPosition: function (typeNumber) { + return QRUtil.PATTERN_POSITION_TABLE[typeNumber - 1]; + }, + + getMask: function (maskPattern, i, j) { + + switch (maskPattern) { + + case QRMaskPattern.PATTERN000: + return (i + j) % 2 == 0; + case QRMaskPattern.PATTERN001: + return i % 2 == 0; + case QRMaskPattern.PATTERN010: + return j % 3 == 0; + case QRMaskPattern.PATTERN011: + return (i + j) % 3 == 0; + case QRMaskPattern.PATTERN100: + return (Math.floor(i / 2) + Math.floor(j / 3)) % 2 == 0; + case QRMaskPattern.PATTERN101: + return i * j % 2 + i * j % 3 == 0; + case QRMaskPattern.PATTERN110: + return (i * j % 2 + i * j % 3) % 2 == 0; + case QRMaskPattern.PATTERN111: + return (i * j % 3 + (i + j) % 2) % 2 == 0; + + default: + throw new Error("bad maskPattern:" + maskPattern); + } + }, + + getErrorCorrectPolynomial: function (errorCorrectLength) { + + var a = new QRPolynomial([1], 0); + + for (var i = 0; i < errorCorrectLength; i++) { + a = a.multiply(new QRPolynomial([1, QRMath.gexp(i)], 0)); + } + + return a; + }, + + getLengthInBits: function (mode, type) { + + if (1 <= type && type < 10) { + + // 1 - 9 + + switch (mode) { + case QRMode.MODE_NUMBER: + return 10; + case QRMode.MODE_ALPHA_NUM: + return 9; + case QRMode.MODE_8BIT_BYTE: + return 8; + case QRMode.MODE_KANJI: + return 8; + default: + throw new Error("mode:" + mode); + } + } else if (type < 27) { + + // 10 - 26 + + switch (mode) { + case QRMode.MODE_NUMBER: + return 12; + case QRMode.MODE_ALPHA_NUM: + return 11; + case QRMode.MODE_8BIT_BYTE: + return 16; + case QRMode.MODE_KANJI: + return 10; + default: + throw new Error("mode:" + mode); + } + } else if (type < 41) { + + // 27 - 40 + + switch (mode) { + case QRMode.MODE_NUMBER: + return 14; + case QRMode.MODE_ALPHA_NUM: + return 13; + case QRMode.MODE_8BIT_BYTE: + return 16; + case QRMode.MODE_KANJI: + return 12; + default: + throw new Error("mode:" + mode); + } + } else { + throw new Error("type:" + type); + } + }, + + getLostPoint: function (qrCode) { + + var moduleCount = qrCode.getModuleCount(); + + var lostPoint = 0; + + // LEVEL1 + + for (var row = 0; row < moduleCount; row++) { + + for (var col = 0; col < moduleCount; col++) { + + var sameCount = 0; + var dark = qrCode.isDark(row, col); + + for (var r = -1; r <= 1; r++) { + + if (row + r < 0 || moduleCount <= row + r) { + continue; + } + + for (var c = -1; c <= 1; c++) { + + if (col + c < 0 || moduleCount <= col + c) { + continue; + } + + if (r == 0 && c == 0) { + continue; + } + + if (dark == qrCode.isDark(row + r, col + c)) { + sameCount++; + } + } + } + + if (sameCount > 5) { + lostPoint += 3 + sameCount - 5; + } + } + } + + // LEVEL2 + + for (var row = 0; row < moduleCount - 1; row++) { + for (var col = 0; col < moduleCount - 1; col++) { + var count = 0; + if (qrCode.isDark(row, col)) count++; + if (qrCode.isDark(row + 1, col)) count++; + if (qrCode.isDark(row, col + 1)) count++; + if (qrCode.isDark(row + 1, col + 1)) count++; + if (count == 0 || count == 4) { + lostPoint += 3; + } + } + } + + // LEVEL3 + + for (var row = 0; row < moduleCount; row++) { + for (var col = 0; col < moduleCount - 6; col++) { + if (qrCode.isDark(row, col) && !qrCode.isDark(row, col + 1) && qrCode.isDark(row, col + 2) && qrCode.isDark(row, col + 3) && qrCode.isDark(row, col + 4) && !qrCode.isDark(row, col + 5) && qrCode.isDark(row, col + 6)) { + lostPoint += 40; + } + } + } + + for (var col = 0; col < moduleCount; col++) { + for (var row = 0; row < moduleCount - 6; row++) { + if (qrCode.isDark(row, col) && !qrCode.isDark(row + 1, col) && qrCode.isDark(row + 2, col) && qrCode.isDark(row + 3, col) && qrCode.isDark(row + 4, col) && !qrCode.isDark(row + 5, col) && qrCode.isDark(row + 6, col)) { + lostPoint += 40; + } + } + } + + // LEVEL4 + + var darkCount = 0; + + for (var col = 0; col < moduleCount; col++) { + for (var row = 0; row < moduleCount; row++) { + if (qrCode.isDark(row, col)) { + darkCount++; + } + } + } + + var ratio = Math.abs(100 * darkCount / moduleCount / moduleCount - 50) / 5; + lostPoint += ratio * 10; + + return lostPoint; + } + +}; + +//--------------------------------------------------------------------- +// QRMath +//--------------------------------------------------------------------- + +var QRMath = { + + glog: function (n) { + + if (n < 1) { + throw new Error("glog(" + n + ")"); + } + + return QRMath.LOG_TABLE[n]; + }, + + gexp: function (n) { + + while (n < 0) { + n += 255; + } + + while (n >= 256) { + n -= 255; + } + + return QRMath.EXP_TABLE[n]; + }, + + EXP_TABLE: new Array(256), + + LOG_TABLE: new Array(256) + +}; + +for (var i = 0; i < 8; i++) { + QRMath.EXP_TABLE[i] = 1 << i; +} +for (var i = 8; i < 256; i++) { + QRMath.EXP_TABLE[i] = QRMath.EXP_TABLE[i - 4] ^ QRMath.EXP_TABLE[i - 5] ^ QRMath.EXP_TABLE[i - 6] ^ QRMath.EXP_TABLE[i - 8]; +} +for (var i = 0; i < 255; i++) { + QRMath.LOG_TABLE[QRMath.EXP_TABLE[i]] = i; +} + +//--------------------------------------------------------------------- +// QRPolynomial +//--------------------------------------------------------------------- + +function QRPolynomial(num, shift) { + + if (num.length == undefined) { + throw new Error(num.length + "/" + shift); + } + + var offset = 0; + + while (offset < num.length && num[offset] == 0) { + offset++; + } + + this.num = new Array(num.length - offset + shift); + for (var i = 0; i < num.length - offset; i++) { + this.num[i] = num[i + offset]; + } +} + +QRPolynomial.prototype = { + + get: function (index) { + return this.num[index]; + }, + + getLength: function () { + return this.num.length; + }, + + multiply: function (e) { + + var num = new Array(this.getLength() + e.getLength() - 1); + + for (var i = 0; i < this.getLength(); i++) { + for (var j = 0; j < e.getLength(); j++) { + num[i + j] ^= QRMath.gexp(QRMath.glog(this.get(i)) + QRMath.glog(e.get(j))); + } + } + + return new QRPolynomial(num, 0); + }, + + mod: function (e) { + + if (this.getLength() - e.getLength() < 0) { + return this; + } + + var ratio = QRMath.glog(this.get(0)) - QRMath.glog(e.get(0)); + + var num = new Array(this.getLength()); + + for (var i = 0; i < this.getLength(); i++) { + num[i] = this.get(i); + } + + for (var i = 0; i < e.getLength(); i++) { + num[i] ^= QRMath.gexp(QRMath.glog(e.get(i)) + ratio); + } + + // recursive call + return new QRPolynomial(num, 0).mod(e); + } +}; + +//--------------------------------------------------------------------- +// QRRSBlock +//--------------------------------------------------------------------- + +function QRRSBlock(totalCount, dataCount) { + this.totalCount = totalCount; + this.dataCount = dataCount; +} + +QRRSBlock.RS_BLOCK_TABLE = [ + +// L +// M +// Q +// H + +// 1 +[1, 26, 19], [1, 26, 16], [1, 26, 13], [1, 26, 9], + +// 2 +[1, 44, 34], [1, 44, 28], [1, 44, 22], [1, 44, 16], + +// 3 +[1, 70, 55], [1, 70, 44], [2, 35, 17], [2, 35, 13], + +// 4 +[1, 100, 80], [2, 50, 32], [2, 50, 24], [4, 25, 9], + +// 5 +[1, 134, 108], [2, 67, 43], [2, 33, 15, 2, 34, 16], [2, 33, 11, 2, 34, 12], + +// 6 +[2, 86, 68], [4, 43, 27], [4, 43, 19], [4, 43, 15], + +// 7 +[2, 98, 78], [4, 49, 31], [2, 32, 14, 4, 33, 15], [4, 39, 13, 1, 40, 14], + +// 8 +[2, 121, 97], [2, 60, 38, 2, 61, 39], [4, 40, 18, 2, 41, 19], [4, 40, 14, 2, 41, 15], + +// 9 +[2, 146, 116], [3, 58, 36, 2, 59, 37], [4, 36, 16, 4, 37, 17], [4, 36, 12, 4, 37, 13], + +// 10 +[2, 86, 68, 2, 87, 69], [4, 69, 43, 1, 70, 44], [6, 43, 19, 2, 44, 20], [6, 43, 15, 2, 44, 16], + +// 11 +[4, 101, 81], [1, 80, 50, 4, 81, 51], [4, 50, 22, 4, 51, 23], [3, 36, 12, 8, 37, 13], + +// 12 +[2, 116, 92, 2, 117, 93], [6, 58, 36, 2, 59, 37], [4, 46, 20, 6, 47, 21], [7, 42, 14, 4, 43, 15], + +// 13 +[4, 133, 107], [8, 59, 37, 1, 60, 38], [8, 44, 20, 4, 45, 21], [12, 33, 11, 4, 34, 12], + +// 14 +[3, 145, 115, 1, 146, 116], [4, 64, 40, 5, 65, 41], [11, 36, 16, 5, 37, 17], [11, 36, 12, 5, 37, 13], + +// 15 +[5, 109, 87, 1, 110, 88], [5, 65, 41, 5, 66, 42], [5, 54, 24, 7, 55, 25], [11, 36, 12], + +// 16 +[5, 122, 98, 1, 123, 99], [7, 73, 45, 3, 74, 46], [15, 43, 19, 2, 44, 20], [3, 45, 15, 13, 46, 16], + +// 17 +[1, 135, 107, 5, 136, 108], [10, 74, 46, 1, 75, 47], [1, 50, 22, 15, 51, 23], [2, 42, 14, 17, 43, 15], + +// 18 +[5, 150, 120, 1, 151, 121], [9, 69, 43, 4, 70, 44], [17, 50, 22, 1, 51, 23], [2, 42, 14, 19, 43, 15], + +// 19 +[3, 141, 113, 4, 142, 114], [3, 70, 44, 11, 71, 45], [17, 47, 21, 4, 48, 22], [9, 39, 13, 16, 40, 14], + +// 20 +[3, 135, 107, 5, 136, 108], [3, 67, 41, 13, 68, 42], [15, 54, 24, 5, 55, 25], [15, 43, 15, 10, 44, 16], + +// 21 +[4, 144, 116, 4, 145, 117], [17, 68, 42], [17, 50, 22, 6, 51, 23], [19, 46, 16, 6, 47, 17], + +// 22 +[2, 139, 111, 7, 140, 112], [17, 74, 46], [7, 54, 24, 16, 55, 25], [34, 37, 13], + +// 23 +[4, 151, 121, 5, 152, 122], [4, 75, 47, 14, 76, 48], [11, 54, 24, 14, 55, 25], [16, 45, 15, 14, 46, 16], + +// 24 +[6, 147, 117, 4, 148, 118], [6, 73, 45, 14, 74, 46], [11, 54, 24, 16, 55, 25], [30, 46, 16, 2, 47, 17], + +// 25 +[8, 132, 106, 4, 133, 107], [8, 75, 47, 13, 76, 48], [7, 54, 24, 22, 55, 25], [22, 45, 15, 13, 46, 16], + +// 26 +[10, 142, 114, 2, 143, 115], [19, 74, 46, 4, 75, 47], [28, 50, 22, 6, 51, 23], [33, 46, 16, 4, 47, 17], + +// 27 +[8, 152, 122, 4, 153, 123], [22, 73, 45, 3, 74, 46], [8, 53, 23, 26, 54, 24], [12, 45, 15, 28, 46, 16], + +// 28 +[3, 147, 117, 10, 148, 118], [3, 73, 45, 23, 74, 46], [4, 54, 24, 31, 55, 25], [11, 45, 15, 31, 46, 16], + +// 29 +[7, 146, 116, 7, 147, 117], [21, 73, 45, 7, 74, 46], [1, 53, 23, 37, 54, 24], [19, 45, 15, 26, 46, 16], + +// 30 +[5, 145, 115, 10, 146, 116], [19, 75, 47, 10, 76, 48], [15, 54, 24, 25, 55, 25], [23, 45, 15, 25, 46, 16], + +// 31 +[13, 145, 115, 3, 146, 116], [2, 74, 46, 29, 75, 47], [42, 54, 24, 1, 55, 25], [23, 45, 15, 28, 46, 16], + +// 32 +[17, 145, 115], [10, 74, 46, 23, 75, 47], [10, 54, 24, 35, 55, 25], [19, 45, 15, 35, 46, 16], + +// 33 +[17, 145, 115, 1, 146, 116], [14, 74, 46, 21, 75, 47], [29, 54, 24, 19, 55, 25], [11, 45, 15, 46, 46, 16], + +// 34 +[13, 145, 115, 6, 146, 116], [14, 74, 46, 23, 75, 47], [44, 54, 24, 7, 55, 25], [59, 46, 16, 1, 47, 17], + +// 35 +[12, 151, 121, 7, 152, 122], [12, 75, 47, 26, 76, 48], [39, 54, 24, 14, 55, 25], [22, 45, 15, 41, 46, 16], + +// 36 +[6, 151, 121, 14, 152, 122], [6, 75, 47, 34, 76, 48], [46, 54, 24, 10, 55, 25], [2, 45, 15, 64, 46, 16], + +// 37 +[17, 152, 122, 4, 153, 123], [29, 74, 46, 14, 75, 47], [49, 54, 24, 10, 55, 25], [24, 45, 15, 46, 46, 16], + +// 38 +[4, 152, 122, 18, 153, 123], [13, 74, 46, 32, 75, 47], [48, 54, 24, 14, 55, 25], [42, 45, 15, 32, 46, 16], + +// 39 +[20, 147, 117, 4, 148, 118], [40, 75, 47, 7, 76, 48], [43, 54, 24, 22, 55, 25], [10, 45, 15, 67, 46, 16], + +// 40 +[19, 148, 118, 6, 149, 119], [18, 75, 47, 31, 76, 48], [34, 54, 24, 34, 55, 25], [20, 45, 15, 61, 46, 16]]; + +QRRSBlock.getRSBlocks = function (typeNumber, errorCorrectLevel) { + + var rsBlock = QRRSBlock.getRsBlockTable(typeNumber, errorCorrectLevel); + + if (rsBlock == undefined) { + throw new Error("bad rs block @ typeNumber:" + typeNumber + "/errorCorrectLevel:" + errorCorrectLevel); + } + + var length = rsBlock.length / 3; + + var list = new Array(); + + for (var i = 0; i < length; i++) { + + var count = rsBlock[i * 3 + 0]; + var totalCount = rsBlock[i * 3 + 1]; + var dataCount = rsBlock[i * 3 + 2]; + + for (var j = 0; j < count; j++) { + list.push(new QRRSBlock(totalCount, dataCount)); + } + } + + return list; +}; + +QRRSBlock.getRsBlockTable = function (typeNumber, errorCorrectLevel) { + + switch (errorCorrectLevel) { + case QRErrorCorrectLevel.L: + return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 0]; + case QRErrorCorrectLevel.M: + return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 1]; + case QRErrorCorrectLevel.Q: + return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 2]; + case QRErrorCorrectLevel.H: + return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 3]; + default: + return undefined; + } +}; + +//--------------------------------------------------------------------- +// QRBitBuffer +//--------------------------------------------------------------------- + +function QRBitBuffer() { + this.buffer = new Array(); + this.length = 0; +} + +QRBitBuffer.prototype = { + + get: function (index) { + var bufIndex = Math.floor(index / 8); + return (this.buffer[bufIndex] >>> 7 - index % 8 & 1) == 1; + }, + + put: function (num, length) { + for (var i = 0; i < length; i++) { + this.putBit((num >>> length - i - 1 & 1) == 1); + } + }, + + getLengthInBits: function () { + return this.length; + }, + + putBit: function (bit) { + + var bufIndex = Math.floor(this.length / 8); + if (this.buffer.length <= bufIndex) { + this.buffer.push(0); + } + + if (bit) { + this.buffer[bufIndex] |= 0x80 >>> this.length % 8; + } + + this.length++; + } +}; + +// support Chinese +function utf16to8(str) { + var out, i, len, c; + out = ''; + len = str.length; + for (i = 0; i < len; i++) { + c = str.charCodeAt(i); + if (c >= 0x0001 && c <= 0x007F) { + out += str.charAt(i); + } else if (c > 0x07FF) { + out += String.fromCharCode(0xE0 | c >> 12 & 0x0F); + out += String.fromCharCode(0x80 | c >> 6 & 0x3F); + out += String.fromCharCode(0x80 | c >> 0 & 0x3F); + } else { + out += String.fromCharCode(0xC0 | c >> 6 & 0x1F); + out += String.fromCharCode(0x80 | c >> 0 & 0x3F); + } + } + return out; +} + +function drawQrcode(options) { + options = options || {}; + options = extend(true, { + 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 + } + }, options); + + if (!options.canvasId && !options.ctx) { + console.warn('please set canvasId or ctx!'); + return; + } + + createCanvas(); + + function createCanvas() { + // create the qrcode itself + var qrcode = new QRCode(options.typeNumber, options.correctLevel); + qrcode.addData(utf16to8(options.text)); + qrcode.make(); + + // get canvas context + var ctx; + if (options.ctx) { + ctx = options.ctx; + } else { + ctx = options._this ? wx.createCanvasContext && wx.createCanvasContext(options.canvasId, options._this) : wx.createCanvasContext && wx.createCanvasContext(options.canvasId); + } + + // compute tileW/tileH based on options.width/options.height + var tileW = options.width / qrcode.getModuleCount(); + var tileH = options.height / qrcode.getModuleCount(); + + // draw in the canvas + for (var row = 0; row < qrcode.getModuleCount(); row++) { + for (var col = 0; col < qrcode.getModuleCount(); col++) { + var style = qrcode.isDark(row, col) ? options.foreground : options.background; + ctx.setFillStyle(style); + var w = Math.ceil((col + 1) * tileW) - Math.floor(col * tileW); + var h = Math.ceil((row + 1) * tileW) - Math.floor(row * tileW); + ctx.fillRect(Math.round(col * tileW) + options.x, Math.round(row * tileH) + options.y, w, h); + } + } + + if (options.image.imageResource) { + ctx.drawImage(options.image.imageResource, options.image.dx, options.image.dy, options.image.dWidth, options.image.dHeight); + } + + ctx.draw(false, function (e) { + options.callback && options.callback(e); + }); + } +} + +return drawQrcode; + +}))); diff --git a/node_modules/weapp-qrcode/dist/weapp.qrcode.min.js b/node_modules/weapp-qrcode/dist/weapp.qrcode.min.js new file mode 100644 index 0000000..27cae47 --- /dev/null +++ b/node_modules/weapp-qrcode/dist/weapp.qrcode.min.js @@ -0,0 +1,5 @@ +/** + * weapp.qrcode.js v1.0.0 (https://github.com/yingye/weapp-qrcode#readme) + */ + +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.drawQrcode=e()}(this,function(){"use strict";var t=Object.prototype.hasOwnProperty,e=Object.prototype.toString,r=Object.defineProperty,o=Object.getOwnPropertyDescriptor,n=function(t){return"function"==typeof Array.isArray?Array.isArray(t):"[object Array]"===e.call(t)},i=function(r){if(!r||"[object Object]"!==e.call(r))return!1;var o,n=t.call(r,"constructor"),i=r.constructor&&r.constructor.prototype&&t.call(r.constructor.prototype,"isPrototypeOf");if(r.constructor&&!n&&!i)return!1;for(o in r);return void 0===o||t.call(r,o)},a=function(t,e){r&&"__proto__"===e.name?r(t,e.name,{enumerable:!0,configurable:!0,value:e.newValue,writable:!0}):t[e.name]=e.newValue},u=function(e,r){if("__proto__"===r){if(!t.call(e,r))return;if(o)return o(e,r).value}return e[r]},s=function t(){var e,r,o,s,h,l,f=arguments[0],g=1,c=arguments.length,d=!1;for("boolean"==typeof f&&(d=f,f=arguments[1]||{},g=2),(null==f||"object"!=typeof f&&"function"!=typeof f)&&(f={});g=7&&this.setupTypeNumber(t),null==this.dataCache&&(this.dataCache=l.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=w.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=w.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 u=0;u<2;u++)if(null==this.modules[o][a-u]){var s=!1;i>>n&1)),w.getMask(e,o,a-u)&&(s=!s),this.modules[o][a-u]=s,-1==--n&&(i++,n=7)}if((o+=r)<0||this.moduleCount<=o){o-=r,r=-r;break}}}},l.PAD0=236,l.PAD1=17,l.createData=function(t,e,r){for(var o=_.getRSBlocks(t,e),n=new D,i=0;i8*u)throw new Error("code length overflow. ("+n.getLengthInBits()+">"+8*u+")");for(n.getLengthInBits()+4<=8*u&&n.put(0,4);n.getLengthInBits()%8!=0;)n.putBit(!1);for(;!(n.getLengthInBits()>=8*u||(n.put(l.PAD0,8),n.getLengthInBits()>=8*u));)n.put(l.PAD1,8);return l.createBytes(n,o)},l.createBytes=function(t,e){for(var r=0,o=0,n=0,i=new Array(e.length),a=new Array(e.length),u=0;u=0?g.get(c):0}}var d=0;for(l=0;l=0;)e^=w.G15<=0;)e^=w.G18<>>=1;return e},getPatternPosition:function(t){return w.PATTERN_POSITION_TABLE[t-1]},getMask:function(t,e,r){switch(t){case c:return(e+r)%2==0;case d:return e%2==0;case m:return r%3==0;case v:return(e+r)%3==0;case p:return(Math.floor(e/2)+Math.floor(r/3))%2==0;case C:return e*r%2+e*r%3==0;case B:return(e*r%2+e*r%3)%2==0;case L:return(e*r%3+(e+r)%2)%2==0;default:throw new Error("bad maskPattern:"+t)}},getErrorCorrectPolynomial:function(t){for(var e=new A([1],0),r=0;r5&&(r+=3+i-5)}for(o=0;o=256;)t-=255;return y.EXP_TABLE[t]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},E=0;E<8;E++)y.EXP_TABLE[E]=1<>>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++}},function(t){t=t||{},(t=s(!0,{width:256,height:256,x:0,y:0,typeNumber:-1,correctLevel:g.H,background:"#ffffff",foreground:"#000000",image:{imageResource:"",dx:0,dy:0,dWidth:100,dHeight:100}},t)).canvasId||t.ctx?function(){var e,r=new l(t.typeNumber,t.correctLevel);r.addData(function(t){var e,r,o,n;for(e="",o=t.length,r=0;r=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}(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= 0x0001) && (c <= 0x007F)) { + out += str.charAt(i) + } else if (c > 0x07FF) { + out += String.fromCharCode(0xE0 | ((c >> 12) & 0x0F)) + out += String.fromCharCode(0x80 | ((c >> 6) & 0x3F)) + out += String.fromCharCode(0x80 | ((c >> 0) & 0x3F)) + } else { + out += String.fromCharCode(0xC0 | ((c >> 6) & 0x1F)) + out += String.fromCharCode(0x80 | ((c >> 0) & 0x3F)) + } + } + return out +} + +function drawQrcode (options) { + options = options || {} + options = extend(true, { + 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 + } + }, options) + + if (!options.canvasId && !options.ctx) { + console.warn('please set canvasId or ctx!') + return + } + + createCanvas() + + function createCanvas () { + // create the qrcode itself + var qrcode = new QRCode(options.typeNumber, options.correctLevel) + qrcode.addData(utf16to8(options.text)) + qrcode.make() + + // get canvas context + var ctx + if (options.ctx) { + ctx = options.ctx + } else { + ctx = options._this ? wx.createCanvasContext && wx.createCanvasContext(options.canvasId, options._this) : wx.createCanvasContext && wx.createCanvasContext(options.canvasId) + } + + // compute tileW/tileH based on options.width/options.height + var tileW = options.width / qrcode.getModuleCount() + var tileH = options.height / qrcode.getModuleCount() + + // draw in the canvas + for (var row = 0; row < qrcode.getModuleCount(); row++) { + for (var col = 0; col < qrcode.getModuleCount(); col++) { + var style = qrcode.isDark(row, col) ? options.foreground : options.background + ctx.setFillStyle(style) + var w = (Math.ceil((col + 1) * tileW) - Math.floor(col * tileW)) + var h = (Math.ceil((row + 1) * tileW) - Math.floor(row * tileW)) + ctx.fillRect(Math.round(col * tileW) + options.x, Math.round(row * tileH) + options.y, w, h) + } + } + + if (options.image.imageResource) { + ctx.drawImage(options.image.imageResource, options.image.dx, options.image.dy, options.image.dWidth, options.image.dHeight) + } + + ctx.draw(false, function (e) { + options.callback && options.callback(e) + }) + } +} + +export default drawQrcode diff --git a/node_modules/weapp-qrcode/src/qrcode.js b/node_modules/weapp-qrcode/src/qrcode.js new file mode 100644 index 0000000..3289c78 --- /dev/null +++ b/node_modules/weapp-qrcode/src/qrcode.js @@ -0,0 +1,1242 @@ +//--------------------------------------------------------------------- +// QRCode for JavaScript +// +// Copyright (c) 2009 Kazuhiko Arase +// +// URL: http://www.d-project.com/ +// +// Licensed under the MIT license: +// http://www.opensource.org/licenses/mit-license.php +// +// The word "QR Code" is registered trademark of +// DENSO WAVE INCORPORATED +// http://www.denso-wave.com/qrcode/faqpatent-e.html +// +//--------------------------------------------------------------------- + +//--------------------------------------------------------------------- +// QR8bitByte +//--------------------------------------------------------------------- + +function QR8bitByte(data) { + this.mode = QRMode.MODE_8BIT_BYTE; + this.data = data; +} + +QR8bitByte.prototype = { + + getLength : function(buffer) { + return this.data.length; + }, + + write : function(buffer) { + for (var i = 0; i < this.data.length; i++) { + // not JIS ... + buffer.put(this.data.charCodeAt(i), 8); + } + } +}; + +//--------------------------------------------------------------------- +// QRCode +//--------------------------------------------------------------------- + +function QRCode(typeNumber, errorCorrectLevel) { + this.typeNumber = typeNumber; + this.errorCorrectLevel = errorCorrectLevel; + this.modules = null; + this.moduleCount = 0; + this.dataCache = null; + this.dataList = new Array(); +} + +QRCode.prototype = { + + addData : function(data) { + var newData = new QR8bitByte(data); + this.dataList.push(newData); + this.dataCache = null; + }, + + isDark : function(row, col) { + if (row < 0 || this.moduleCount <= row || col < 0 || this.moduleCount <= col) { + throw new Error(row + "," + col); + } + return this.modules[row][col]; + }, + + getModuleCount : function() { + return this.moduleCount; + }, + + make : function() { + // Calculate automatically typeNumber if provided is < 1 + if (this.typeNumber < 1 ){ + var typeNumber = 1; + for (typeNumber = 1; typeNumber < 40; typeNumber++) { + var rsBlocks = QRRSBlock.getRSBlocks(typeNumber, this.errorCorrectLevel); + + var buffer = new QRBitBuffer(); + var totalDataCount = 0; + for (var i = 0; i < rsBlocks.length; i++) { + totalDataCount += rsBlocks[i].dataCount; + } + + for (var i = 0; i < this.dataList.length; i++) { + var data = this.dataList[i]; + buffer.put(data.mode, 4); + buffer.put(data.getLength(), QRUtil.getLengthInBits(data.mode, typeNumber) ); + data.write(buffer); + } + if (buffer.getLengthInBits() <= totalDataCount * 8) + break; + } + this.typeNumber = typeNumber; + } + this.makeImpl(false, this.getBestMaskPattern() ); + }, + + makeImpl : function(test, maskPattern) { + + this.moduleCount = this.typeNumber * 4 + 17; + this.modules = new Array(this.moduleCount); + + for (var row = 0; row < this.moduleCount; row++) { + + this.modules[row] = new Array(this.moduleCount); + + for (var col = 0; col < this.moduleCount; col++) { + this.modules[row][col] = null;//(col + row) % 3; + } + } + + this.setupPositionProbePattern(0, 0); + this.setupPositionProbePattern(this.moduleCount - 7, 0); + this.setupPositionProbePattern(0, this.moduleCount - 7); + this.setupPositionAdjustPattern(); + this.setupTimingPattern(); + this.setupTypeInfo(test, maskPattern); + + if (this.typeNumber >= 7) { + this.setupTypeNumber(test); + } + + if (this.dataCache == null) { + this.dataCache = QRCode.createData(this.typeNumber, this.errorCorrectLevel, this.dataList); + } + + this.mapData(this.dataCache, maskPattern); + }, + + setupPositionProbePattern : function(row, col) { + + for (var r = -1; r <= 7; r++) { + + if (row + r <= -1 || this.moduleCount <= row + r) continue; + + for (var c = -1; c <= 7; c++) { + + if (col + c <= -1 || this.moduleCount <= col + c) continue; + + if ( (0 <= r && r <= 6 && (c == 0 || c == 6) ) + || (0 <= c && c <= 6 && (r == 0 || r == 6) ) + || (2 <= r && r <= 4 && 2 <= c && c <= 4) ) { + this.modules[row + r][col + c] = true; + } else { + this.modules[row + r][col + c] = false; + } + } + } + }, + + getBestMaskPattern : function() { + + var minLostPoint = 0; + var pattern = 0; + + for (var i = 0; i < 8; i++) { + + this.makeImpl(true, i); + + var lostPoint = QRUtil.getLostPoint(this); + + if (i == 0 || minLostPoint > lostPoint) { + minLostPoint = lostPoint; + pattern = i; + } + } + + return pattern; + }, + + createMovieClip : function(target_mc, instance_name, depth) { + + var qr_mc = target_mc.createEmptyMovieClip(instance_name, depth); + var cs = 1; + + this.make(); + + for (var row = 0; row < this.modules.length; row++) { + + var y = row * cs; + + for (var col = 0; col < this.modules[row].length; col++) { + + var x = col * cs; + var dark = this.modules[row][col]; + + if (dark) { + qr_mc.beginFill(0, 100); + qr_mc.moveTo(x, y); + qr_mc.lineTo(x + cs, y); + qr_mc.lineTo(x + cs, y + cs); + qr_mc.lineTo(x, y + cs); + qr_mc.endFill(); + } + } + } + + return qr_mc; + }, + + setupTimingPattern : function() { + + for (var r = 8; r < this.moduleCount - 8; r++) { + if (this.modules[r][6] != null) { + continue; + } + this.modules[r][6] = (r % 2 == 0); + } + + for (var c = 8; c < this.moduleCount - 8; c++) { + if (this.modules[6][c] != null) { + continue; + } + this.modules[6][c] = (c % 2 == 0); + } + }, + + setupPositionAdjustPattern : function() { + + var pos = QRUtil.getPatternPosition(this.typeNumber); + + for (var i = 0; i < pos.length; i++) { + + for (var j = 0; j < pos.length; j++) { + + var row = pos[i]; + var col = pos[j]; + + if (this.modules[row][col] != null) { + continue; + } + + for (var r = -2; r <= 2; r++) { + + for (var c = -2; c <= 2; c++) { + + if (r == -2 || r == 2 || c == -2 || c == 2 + || (r == 0 && c == 0) ) { + this.modules[row + r][col + c] = true; + } else { + this.modules[row + r][col + c] = false; + } + } + } + } + } + }, + + setupTypeNumber : function(test) { + + var bits = QRUtil.getBCHTypeNumber(this.typeNumber); + + for (var i = 0; i < 18; i++) { + var mod = (!test && ( (bits >> i) & 1) == 1); + this.modules[Math.floor(i / 3)][i % 3 + this.moduleCount - 8 - 3] = mod; + } + + for (var i = 0; i < 18; i++) { + var mod = (!test && ( (bits >> i) & 1) == 1); + this.modules[i % 3 + this.moduleCount - 8 - 3][Math.floor(i / 3)] = mod; + } + }, + + setupTypeInfo : function(test, maskPattern) { + + var data = (this.errorCorrectLevel << 3) | maskPattern; + var bits = QRUtil.getBCHTypeInfo(data); + + // vertical + for (var i = 0; i < 15; i++) { + + var mod = (!test && ( (bits >> i) & 1) == 1); + + if (i < 6) { + this.modules[i][8] = mod; + } else if (i < 8) { + this.modules[i + 1][8] = mod; + } else { + this.modules[this.moduleCount - 15 + i][8] = mod; + } + } + + // horizontal + for (var i = 0; i < 15; i++) { + + var mod = (!test && ( (bits >> i) & 1) == 1); + + if (i < 8) { + this.modules[8][this.moduleCount - i - 1] = mod; + } else if (i < 9) { + this.modules[8][15 - i - 1 + 1] = mod; + } else { + this.modules[8][15 - i - 1] = mod; + } + } + + // fixed module + this.modules[this.moduleCount - 8][8] = (!test); + + }, + + mapData : function(data, maskPattern) { + + var inc = -1; + var row = this.moduleCount - 1; + var bitIndex = 7; + var byteIndex = 0; + + for (var col = this.moduleCount - 1; col > 0; col -= 2) { + + if (col == 6) col--; + + while (true) { + + for (var c = 0; c < 2; c++) { + + if (this.modules[row][col - c] == null) { + + var dark = false; + + if (byteIndex < data.length) { + dark = ( ( (data[byteIndex] >>> bitIndex) & 1) == 1); + } + + var mask = QRUtil.getMask(maskPattern, row, col - c); + + if (mask) { + dark = !dark; + } + + this.modules[row][col - c] = dark; + bitIndex--; + + if (bitIndex == -1) { + byteIndex++; + bitIndex = 7; + } + } + } + + row += inc; + + if (row < 0 || this.moduleCount <= row) { + row -= inc; + inc = -inc; + break; + } + } + } + + } + +}; + +QRCode.PAD0 = 0xEC; +QRCode.PAD1 = 0x11; + +QRCode.createData = function(typeNumber, errorCorrectLevel, dataList) { + + var rsBlocks = QRRSBlock.getRSBlocks(typeNumber, errorCorrectLevel); + + var buffer = new QRBitBuffer(); + + for (var i = 0; i < dataList.length; i++) { + var data = dataList[i]; + buffer.put(data.mode, 4); + buffer.put(data.getLength(), QRUtil.getLengthInBits(data.mode, typeNumber) ); + data.write(buffer); + } + + // calc num max data. + var totalDataCount = 0; + for (var i = 0; i < rsBlocks.length; i++) { + totalDataCount += rsBlocks[i].dataCount; + } + + if (buffer.getLengthInBits() > totalDataCount * 8) { + throw new Error("code length overflow. (" + + buffer.getLengthInBits() + + ">" + + totalDataCount * 8 + + ")"); + } + + // end code + if (buffer.getLengthInBits() + 4 <= totalDataCount * 8) { + buffer.put(0, 4); + } + + // padding + while (buffer.getLengthInBits() % 8 != 0) { + buffer.putBit(false); + } + + // padding + while (true) { + + if (buffer.getLengthInBits() >= totalDataCount * 8) { + break; + } + buffer.put(QRCode.PAD0, 8); + + if (buffer.getLengthInBits() >= totalDataCount * 8) { + break; + } + buffer.put(QRCode.PAD1, 8); + } + + return QRCode.createBytes(buffer, rsBlocks); +} + +QRCode.createBytes = function(buffer, rsBlocks) { + + var offset = 0; + + var maxDcCount = 0; + var maxEcCount = 0; + + var dcdata = new Array(rsBlocks.length); + var ecdata = new Array(rsBlocks.length); + + for (var r = 0; r < rsBlocks.length; r++) { + + var dcCount = rsBlocks[r].dataCount; + var ecCount = rsBlocks[r].totalCount - dcCount; + + maxDcCount = Math.max(maxDcCount, dcCount); + maxEcCount = Math.max(maxEcCount, ecCount); + + dcdata[r] = new Array(dcCount); + + for (var i = 0; i < dcdata[r].length; i++) { + dcdata[r][i] = 0xff & buffer.buffer[i + offset]; + } + offset += dcCount; + + var rsPoly = QRUtil.getErrorCorrectPolynomial(ecCount); + var rawPoly = new QRPolynomial(dcdata[r], rsPoly.getLength() - 1); + + var modPoly = rawPoly.mod(rsPoly); + ecdata[r] = new Array(rsPoly.getLength() - 1); + for (var i = 0; i < ecdata[r].length; i++) { + var modIndex = i + modPoly.getLength() - ecdata[r].length; + ecdata[r][i] = (modIndex >= 0)? modPoly.get(modIndex) : 0; + } + + } + + var totalCodeCount = 0; + for (var i = 0; i < rsBlocks.length; i++) { + totalCodeCount += rsBlocks[i].totalCount; + } + + var data = new Array(totalCodeCount); + var index = 0; + + for (var i = 0; i < maxDcCount; i++) { + for (var r = 0; r < rsBlocks.length; r++) { + if (i < dcdata[r].length) { + data[index++] = dcdata[r][i]; + } + } + } + + for (var i = 0; i < maxEcCount; i++) { + for (var r = 0; r < rsBlocks.length; r++) { + if (i < ecdata[r].length) { + data[index++] = ecdata[r][i]; + } + } + } + + return data; + +} + +//--------------------------------------------------------------------- +// QRMode +//--------------------------------------------------------------------- + +var QRMode = { + MODE_NUMBER : 1 << 0, + MODE_ALPHA_NUM : 1 << 1, + MODE_8BIT_BYTE : 1 << 2, + MODE_KANJI : 1 << 3 +}; + +//--------------------------------------------------------------------- +// QRErrorCorrectLevel +//--------------------------------------------------------------------- + +var QRErrorCorrectLevel = { + L : 1, + M : 0, + Q : 3, + H : 2 +}; + +//--------------------------------------------------------------------- +// QRMaskPattern +//--------------------------------------------------------------------- + +var QRMaskPattern = { + PATTERN000 : 0, + PATTERN001 : 1, + PATTERN010 : 2, + PATTERN011 : 3, + PATTERN100 : 4, + PATTERN101 : 5, + PATTERN110 : 6, + PATTERN111 : 7 +}; + +//--------------------------------------------------------------------- +// QRUtil +//--------------------------------------------------------------------- + +var QRUtil = { + + PATTERN_POSITION_TABLE : [ + [], + [6, 18], + [6, 22], + [6, 26], + [6, 30], + [6, 34], + [6, 22, 38], + [6, 24, 42], + [6, 26, 46], + [6, 28, 50], + [6, 30, 54], + [6, 32, 58], + [6, 34, 62], + [6, 26, 46, 66], + [6, 26, 48, 70], + [6, 26, 50, 74], + [6, 30, 54, 78], + [6, 30, 56, 82], + [6, 30, 58, 86], + [6, 34, 62, 90], + [6, 28, 50, 72, 94], + [6, 26, 50, 74, 98], + [6, 30, 54, 78, 102], + [6, 28, 54, 80, 106], + [6, 32, 58, 84, 110], + [6, 30, 58, 86, 114], + [6, 34, 62, 90, 118], + [6, 26, 50, 74, 98, 122], + [6, 30, 54, 78, 102, 126], + [6, 26, 52, 78, 104, 130], + [6, 30, 56, 82, 108, 134], + [6, 34, 60, 86, 112, 138], + [6, 30, 58, 86, 114, 142], + [6, 34, 62, 90, 118, 146], + [6, 30, 54, 78, 102, 126, 150], + [6, 24, 50, 76, 102, 128, 154], + [6, 28, 54, 80, 106, 132, 158], + [6, 32, 58, 84, 110, 136, 162], + [6, 26, 54, 82, 110, 138, 166], + [6, 30, 58, 86, 114, 142, 170] + ], + + G15 : (1 << 10) | (1 << 8) | (1 << 5) | (1 << 4) | (1 << 2) | (1 << 1) | (1 << 0), + G18 : (1 << 12) | (1 << 11) | (1 << 10) | (1 << 9) | (1 << 8) | (1 << 5) | (1 << 2) | (1 << 0), + G15_MASK : (1 << 14) | (1 << 12) | (1 << 10) | (1 << 4) | (1 << 1), + + getBCHTypeInfo : function(data) { + var d = data << 10; + while (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G15) >= 0) { + d ^= (QRUtil.G15 << (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G15) ) ); + } + return ( (data << 10) | d) ^ QRUtil.G15_MASK; + }, + + getBCHTypeNumber : function(data) { + var d = data << 12; + while (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G18) >= 0) { + d ^= (QRUtil.G18 << (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G18) ) ); + } + return (data << 12) | d; + }, + + getBCHDigit : function(data) { + + var digit = 0; + + while (data != 0) { + digit++; + data >>>= 1; + } + + return digit; + }, + + getPatternPosition : function(typeNumber) { + return QRUtil.PATTERN_POSITION_TABLE[typeNumber - 1]; + }, + + getMask : function(maskPattern, i, j) { + + switch (maskPattern) { + + case QRMaskPattern.PATTERN000 : return (i + j) % 2 == 0; + case QRMaskPattern.PATTERN001 : return i % 2 == 0; + case QRMaskPattern.PATTERN010 : return j % 3 == 0; + case QRMaskPattern.PATTERN011 : return (i + j) % 3 == 0; + case QRMaskPattern.PATTERN100 : return (Math.floor(i / 2) + Math.floor(j / 3) ) % 2 == 0; + case QRMaskPattern.PATTERN101 : return (i * j) % 2 + (i * j) % 3 == 0; + case QRMaskPattern.PATTERN110 : return ( (i * j) % 2 + (i * j) % 3) % 2 == 0; + case QRMaskPattern.PATTERN111 : return ( (i * j) % 3 + (i + j) % 2) % 2 == 0; + + default : + throw new Error("bad maskPattern:" + maskPattern); + } + }, + + getErrorCorrectPolynomial : function(errorCorrectLength) { + + var a = new QRPolynomial([1], 0); + + for (var i = 0; i < errorCorrectLength; i++) { + a = a.multiply(new QRPolynomial([1, QRMath.gexp(i)], 0) ); + } + + return a; + }, + + getLengthInBits : function(mode, type) { + + if (1 <= type && type < 10) { + + // 1 - 9 + + switch(mode) { + case QRMode.MODE_NUMBER : return 10; + case QRMode.MODE_ALPHA_NUM : return 9; + case QRMode.MODE_8BIT_BYTE : return 8; + case QRMode.MODE_KANJI : return 8; + default : + throw new Error("mode:" + mode); + } + + } else if (type < 27) { + + // 10 - 26 + + switch(mode) { + case QRMode.MODE_NUMBER : return 12; + case QRMode.MODE_ALPHA_NUM : return 11; + case QRMode.MODE_8BIT_BYTE : return 16; + case QRMode.MODE_KANJI : return 10; + default : + throw new Error("mode:" + mode); + } + + } else if (type < 41) { + + // 27 - 40 + + switch(mode) { + case QRMode.MODE_NUMBER : return 14; + case QRMode.MODE_ALPHA_NUM : return 13; + case QRMode.MODE_8BIT_BYTE : return 16; + case QRMode.MODE_KANJI : return 12; + default : + throw new Error("mode:" + mode); + } + + } else { + throw new Error("type:" + type); + } + }, + + getLostPoint : function(qrCode) { + + var moduleCount = qrCode.getModuleCount(); + + var lostPoint = 0; + + // LEVEL1 + + for (var row = 0; row < moduleCount; row++) { + + for (var col = 0; col < moduleCount; col++) { + + var sameCount = 0; + var dark = qrCode.isDark(row, col); + + for (var r = -1; r <= 1; r++) { + + if (row + r < 0 || moduleCount <= row + r) { + continue; + } + + for (var c = -1; c <= 1; c++) { + + if (col + c < 0 || moduleCount <= col + c) { + continue; + } + + if (r == 0 && c == 0) { + continue; + } + + if (dark == qrCode.isDark(row + r, col + c) ) { + sameCount++; + } + } + } + + if (sameCount > 5) { + lostPoint += (3 + sameCount - 5); + } + } + } + + // LEVEL2 + + for (var row = 0; row < moduleCount - 1; row++) { + for (var col = 0; col < moduleCount - 1; col++) { + var count = 0; + if (qrCode.isDark(row, col ) ) count++; + if (qrCode.isDark(row + 1, col ) ) count++; + if (qrCode.isDark(row, col + 1) ) count++; + if (qrCode.isDark(row + 1, col + 1) ) count++; + if (count == 0 || count == 4) { + lostPoint += 3; + } + } + } + + // LEVEL3 + + for (var row = 0; row < moduleCount; row++) { + for (var col = 0; col < moduleCount - 6; col++) { + if (qrCode.isDark(row, col) + && !qrCode.isDark(row, col + 1) + && qrCode.isDark(row, col + 2) + && qrCode.isDark(row, col + 3) + && qrCode.isDark(row, col + 4) + && !qrCode.isDark(row, col + 5) + && qrCode.isDark(row, col + 6) ) { + lostPoint += 40; + } + } + } + + for (var col = 0; col < moduleCount; col++) { + for (var row = 0; row < moduleCount - 6; row++) { + if (qrCode.isDark(row, col) + && !qrCode.isDark(row + 1, col) + && qrCode.isDark(row + 2, col) + && qrCode.isDark(row + 3, col) + && qrCode.isDark(row + 4, col) + && !qrCode.isDark(row + 5, col) + && qrCode.isDark(row + 6, col) ) { + lostPoint += 40; + } + } + } + + // LEVEL4 + + var darkCount = 0; + + for (var col = 0; col < moduleCount; col++) { + for (var row = 0; row < moduleCount; row++) { + if (qrCode.isDark(row, col) ) { + darkCount++; + } + } + } + + var ratio = Math.abs(100 * darkCount / moduleCount / moduleCount - 50) / 5; + lostPoint += ratio * 10; + + return lostPoint; + } + +}; + + +//--------------------------------------------------------------------- +// QRMath +//--------------------------------------------------------------------- + +var QRMath = { + + glog : function(n) { + + if (n < 1) { + throw new Error("glog(" + n + ")"); + } + + return QRMath.LOG_TABLE[n]; + }, + + gexp : function(n) { + + while (n < 0) { + n += 255; + } + + while (n >= 256) { + n -= 255; + } + + return QRMath.EXP_TABLE[n]; + }, + + EXP_TABLE : new Array(256), + + LOG_TABLE : new Array(256) + +}; + +for (var i = 0; i < 8; i++) { + QRMath.EXP_TABLE[i] = 1 << i; +} +for (var i = 8; i < 256; i++) { + QRMath.EXP_TABLE[i] = QRMath.EXP_TABLE[i - 4] + ^ QRMath.EXP_TABLE[i - 5] + ^ QRMath.EXP_TABLE[i - 6] + ^ QRMath.EXP_TABLE[i - 8]; +} +for (var i = 0; i < 255; i++) { + QRMath.LOG_TABLE[QRMath.EXP_TABLE[i] ] = i; +} + +//--------------------------------------------------------------------- +// QRPolynomial +//--------------------------------------------------------------------- + +function QRPolynomial(num, shift) { + + if (num.length == undefined) { + throw new Error(num.length + "/" + shift); + } + + var offset = 0; + + while (offset < num.length && num[offset] == 0) { + offset++; + } + + this.num = new Array(num.length - offset + shift); + for (var i = 0; i < num.length - offset; i++) { + this.num[i] = num[i + offset]; + } +} + +QRPolynomial.prototype = { + + get : function(index) { + return this.num[index]; + }, + + getLength : function() { + return this.num.length; + }, + + multiply : function(e) { + + var num = new Array(this.getLength() + e.getLength() - 1); + + for (var i = 0; i < this.getLength(); i++) { + for (var j = 0; j < e.getLength(); j++) { + num[i + j] ^= QRMath.gexp(QRMath.glog(this.get(i) ) + QRMath.glog(e.get(j) ) ); + } + } + + return new QRPolynomial(num, 0); + }, + + mod : function(e) { + + if (this.getLength() - e.getLength() < 0) { + return this; + } + + var ratio = QRMath.glog(this.get(0) ) - QRMath.glog(e.get(0) ); + + var num = new Array(this.getLength() ); + + for (var i = 0; i < this.getLength(); i++) { + num[i] = this.get(i); + } + + for (var i = 0; i < e.getLength(); i++) { + num[i] ^= QRMath.gexp(QRMath.glog(e.get(i) ) + ratio); + } + + // recursive call + return new QRPolynomial(num, 0).mod(e); + } +}; + +//--------------------------------------------------------------------- +// QRRSBlock +//--------------------------------------------------------------------- + +function QRRSBlock(totalCount, dataCount) { + this.totalCount = totalCount; + this.dataCount = dataCount; +} + +QRRSBlock.RS_BLOCK_TABLE = [ + + // L + // M + // Q + // H + + // 1 + [1, 26, 19], + [1, 26, 16], + [1, 26, 13], + [1, 26, 9], + + // 2 + [1, 44, 34], + [1, 44, 28], + [1, 44, 22], + [1, 44, 16], + + // 3 + [1, 70, 55], + [1, 70, 44], + [2, 35, 17], + [2, 35, 13], + + // 4 + [1, 100, 80], + [2, 50, 32], + [2, 50, 24], + [4, 25, 9], + + // 5 + [1, 134, 108], + [2, 67, 43], + [2, 33, 15, 2, 34, 16], + [2, 33, 11, 2, 34, 12], + + // 6 + [2, 86, 68], + [4, 43, 27], + [4, 43, 19], + [4, 43, 15], + + // 7 + [2, 98, 78], + [4, 49, 31], + [2, 32, 14, 4, 33, 15], + [4, 39, 13, 1, 40, 14], + + // 8 + [2, 121, 97], + [2, 60, 38, 2, 61, 39], + [4, 40, 18, 2, 41, 19], + [4, 40, 14, 2, 41, 15], + + // 9 + [2, 146, 116], + [3, 58, 36, 2, 59, 37], + [4, 36, 16, 4, 37, 17], + [4, 36, 12, 4, 37, 13], + + // 10 + [2, 86, 68, 2, 87, 69], + [4, 69, 43, 1, 70, 44], + [6, 43, 19, 2, 44, 20], + [6, 43, 15, 2, 44, 16], + + // 11 + [4, 101, 81], + [1, 80, 50, 4, 81, 51], + [4, 50, 22, 4, 51, 23], + [3, 36, 12, 8, 37, 13], + + // 12 + [2, 116, 92, 2, 117, 93], + [6, 58, 36, 2, 59, 37], + [4, 46, 20, 6, 47, 21], + [7, 42, 14, 4, 43, 15], + + // 13 + [4, 133, 107], + [8, 59, 37, 1, 60, 38], + [8, 44, 20, 4, 45, 21], + [12, 33, 11, 4, 34, 12], + + // 14 + [3, 145, 115, 1, 146, 116], + [4, 64, 40, 5, 65, 41], + [11, 36, 16, 5, 37, 17], + [11, 36, 12, 5, 37, 13], + + // 15 + [5, 109, 87, 1, 110, 88], + [5, 65, 41, 5, 66, 42], + [5, 54, 24, 7, 55, 25], + [11, 36, 12], + + // 16 + [5, 122, 98, 1, 123, 99], + [7, 73, 45, 3, 74, 46], + [15, 43, 19, 2, 44, 20], + [3, 45, 15, 13, 46, 16], + + // 17 + [1, 135, 107, 5, 136, 108], + [10, 74, 46, 1, 75, 47], + [1, 50, 22, 15, 51, 23], + [2, 42, 14, 17, 43, 15], + + // 18 + [5, 150, 120, 1, 151, 121], + [9, 69, 43, 4, 70, 44], + [17, 50, 22, 1, 51, 23], + [2, 42, 14, 19, 43, 15], + + // 19 + [3, 141, 113, 4, 142, 114], + [3, 70, 44, 11, 71, 45], + [17, 47, 21, 4, 48, 22], + [9, 39, 13, 16, 40, 14], + + // 20 + [3, 135, 107, 5, 136, 108], + [3, 67, 41, 13, 68, 42], + [15, 54, 24, 5, 55, 25], + [15, 43, 15, 10, 44, 16], + + // 21 + [4, 144, 116, 4, 145, 117], + [17, 68, 42], + [17, 50, 22, 6, 51, 23], + [19, 46, 16, 6, 47, 17], + + // 22 + [2, 139, 111, 7, 140, 112], + [17, 74, 46], + [7, 54, 24, 16, 55, 25], + [34, 37, 13], + + // 23 + [4, 151, 121, 5, 152, 122], + [4, 75, 47, 14, 76, 48], + [11, 54, 24, 14, 55, 25], + [16, 45, 15, 14, 46, 16], + + // 24 + [6, 147, 117, 4, 148, 118], + [6, 73, 45, 14, 74, 46], + [11, 54, 24, 16, 55, 25], + [30, 46, 16, 2, 47, 17], + + // 25 + [8, 132, 106, 4, 133, 107], + [8, 75, 47, 13, 76, 48], + [7, 54, 24, 22, 55, 25], + [22, 45, 15, 13, 46, 16], + + // 26 + [10, 142, 114, 2, 143, 115], + [19, 74, 46, 4, 75, 47], + [28, 50, 22, 6, 51, 23], + [33, 46, 16, 4, 47, 17], + + // 27 + [8, 152, 122, 4, 153, 123], + [22, 73, 45, 3, 74, 46], + [8, 53, 23, 26, 54, 24], + [12, 45, 15, 28, 46, 16], + + // 28 + [3, 147, 117, 10, 148, 118], + [3, 73, 45, 23, 74, 46], + [4, 54, 24, 31, 55, 25], + [11, 45, 15, 31, 46, 16], + + // 29 + [7, 146, 116, 7, 147, 117], + [21, 73, 45, 7, 74, 46], + [1, 53, 23, 37, 54, 24], + [19, 45, 15, 26, 46, 16], + + // 30 + [5, 145, 115, 10, 146, 116], + [19, 75, 47, 10, 76, 48], + [15, 54, 24, 25, 55, 25], + [23, 45, 15, 25, 46, 16], + + // 31 + [13, 145, 115, 3, 146, 116], + [2, 74, 46, 29, 75, 47], + [42, 54, 24, 1, 55, 25], + [23, 45, 15, 28, 46, 16], + + // 32 + [17, 145, 115], + [10, 74, 46, 23, 75, 47], + [10, 54, 24, 35, 55, 25], + [19, 45, 15, 35, 46, 16], + + // 33 + [17, 145, 115, 1, 146, 116], + [14, 74, 46, 21, 75, 47], + [29, 54, 24, 19, 55, 25], + [11, 45, 15, 46, 46, 16], + + // 34 + [13, 145, 115, 6, 146, 116], + [14, 74, 46, 23, 75, 47], + [44, 54, 24, 7, 55, 25], + [59, 46, 16, 1, 47, 17], + + // 35 + [12, 151, 121, 7, 152, 122], + [12, 75, 47, 26, 76, 48], + [39, 54, 24, 14, 55, 25], + [22, 45, 15, 41, 46, 16], + + // 36 + [6, 151, 121, 14, 152, 122], + [6, 75, 47, 34, 76, 48], + [46, 54, 24, 10, 55, 25], + [2, 45, 15, 64, 46, 16], + + // 37 + [17, 152, 122, 4, 153, 123], + [29, 74, 46, 14, 75, 47], + [49, 54, 24, 10, 55, 25], + [24, 45, 15, 46, 46, 16], + + // 38 + [4, 152, 122, 18, 153, 123], + [13, 74, 46, 32, 75, 47], + [48, 54, 24, 14, 55, 25], + [42, 45, 15, 32, 46, 16], + + // 39 + [20, 147, 117, 4, 148, 118], + [40, 75, 47, 7, 76, 48], + [43, 54, 24, 22, 55, 25], + [10, 45, 15, 67, 46, 16], + + // 40 + [19, 148, 118, 6, 149, 119], + [18, 75, 47, 31, 76, 48], + [34, 54, 24, 34, 55, 25], + [20, 45, 15, 61, 46, 16] +]; + +QRRSBlock.getRSBlocks = function(typeNumber, errorCorrectLevel) { + + var rsBlock = QRRSBlock.getRsBlockTable(typeNumber, errorCorrectLevel); + + if (rsBlock == undefined) { + throw new Error("bad rs block @ typeNumber:" + typeNumber + "/errorCorrectLevel:" + errorCorrectLevel); + } + + var length = rsBlock.length / 3; + + var list = new Array(); + + for (var i = 0; i < length; i++) { + + var count = rsBlock[i * 3 + 0]; + var totalCount = rsBlock[i * 3 + 1]; + var dataCount = rsBlock[i * 3 + 2]; + + for (var j = 0; j < count; j++) { + list.push(new QRRSBlock(totalCount, dataCount) ); + } + } + + return list; +} + +QRRSBlock.getRsBlockTable = function(typeNumber, errorCorrectLevel) { + + switch(errorCorrectLevel) { + case QRErrorCorrectLevel.L : + return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 0]; + case QRErrorCorrectLevel.M : + return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 1]; + case QRErrorCorrectLevel.Q : + return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 2]; + case QRErrorCorrectLevel.H : + return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 3]; + default : + return undefined; + } +} + +//--------------------------------------------------------------------- +// QRBitBuffer +//--------------------------------------------------------------------- + +function QRBitBuffer() { + this.buffer = new Array(); + this.length = 0; +} + +QRBitBuffer.prototype = { + + get : function(index) { + var bufIndex = Math.floor(index / 8); + return ( (this.buffer[bufIndex] >>> (7 - index % 8) ) & 1) == 1; + }, + + put : function(num, length) { + for (var i = 0; i < length; i++) { + this.putBit( ( (num >>> (length - i - 1) ) & 1) == 1); + } + }, + + getLengthInBits : function() { + return this.length; + }, + + putBit : function(bit) { + + var bufIndex = Math.floor(this.length / 8); + if (this.buffer.length <= bufIndex) { + this.buffer.push(0); + } + + if (bit) { + this.buffer[bufIndex] |= (0x80 >>> (this.length % 8) ); + } + + this.length++; + } +}; + +export { + QRCode, + QRErrorCorrectLevel +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..e4f395f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,40 @@ +{ + "name": "miniapp", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@jdmini/api": "^1.0.10", + "@jdmini/components": "^1.0.6", + "weapp-qrcode": "^1.0.0" + } + }, + "node_modules/@jdmini/api": { + "version": "1.0.10", + "resolved": "https://registry.npmmirror.com/@jdmini/api/-/api-1.0.10.tgz", + "integrity": "sha512-bVFU0awuY033mUT4QqArrYbrnPkBaBFKHoqCMHTVnRCk4b6gTs+cCGDH8uyf2t8ybCgWITKxaaH4Vjzyq8VF8g==" + }, + "node_modules/@jdmini/components": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/@jdmini/components/-/components-1.0.6.tgz", + "integrity": "sha512-ndva1nlZ1QJqDVgHfB0GPxMGmXsZ7SbWjUkRm/WoQIkow75fFbaQCW/xhtQQ+bPbJLjXmCg2p2356klsLLib8A==", + "peerDependencies": { + "@jdmini/api": ">=1.0.8" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/weapp-qrcode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/weapp-qrcode/-/weapp-qrcode-1.0.0.tgz", + "integrity": "sha512-4sa3W0rGDVJ9QaeZpAKlAuUxVyjhDwiUqHyGK/jJMsRMXnhb4yO8qWU/pZruMo+iT5J6CraS67lDMFb1VY+RaA==", + "dependencies": { + "extend": "^3.0.2" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..88955cc --- /dev/null +++ b/package.json @@ -0,0 +1,7 @@ +{ + "dependencies": { + "@jdmini/api": "^1.0.10", + "@jdmini/components": "^1.0.6", + "weapp-qrcode": "^1.0.0" + } +} diff --git a/pages/game/create/create.js b/pages/game/create/create.js new file mode 100644 index 0000000..e3d6744 --- /dev/null +++ b/pages/game/create/create.js @@ -0,0 +1,269 @@ +// pages/game/create/create.js +import request from '../../../utils/request' + +Page({ + data: { + roomName: '', + gameType: 'mahjong', // 默认麻将 + gameTypes: [ + { value: 'mahjong', label: '麻将' }, + { value: 'poker', label: '扑克' }, + { value: 'other', label: '其他' } + ], + tableFee: '0', + quickSettings: ['10', '20', '30'], + showSuccessModal: false, + inviteCode: '', + roomCode: '', + roomId: null, + creating: false, + navbarHeight: 0, + qrcodeUrl: '' + }, + + onLoad() { + // 计算导航栏高度 + const windowInfo = wx.getWindowInfo() + const menuButtonInfo = wx.getMenuButtonBoundingClientRect() + const statusBarHeight = windowInfo.statusBarHeight + const menuButtonTop = menuButtonInfo.top + const navbarHeight = menuButtonInfo.bottom + (menuButtonTop - statusBarHeight) + this.setData({ navbarHeight }) + + // 检查登录状态 + const app = getApp() + const userInfo = app.getUserInfo() + if (!userInfo) { + wx.showToast({ + title: '请先登录', + icon: 'none' + }) + setTimeout(() => { + wx.navigateBack() + }, 1500) + return + } + + // 设置默认房间名称为"昵称的房间" + this.setData({ + roomName: `${userInfo.nickname}的房间` + }) + }, + + // 游戏类型选择 + onGameTypeChange(e) { + const value = e.currentTarget.dataset.value + this.setData({ + gameType: value + }) + }, + + // 桌位费输入 + onTableFeeInput(e) { + let value = e.detail.value + // 只允许正整数 + value = value.replace(/[^\d]/g, '') + this.setData({ + tableFee: value + }) + }, + + // 快捷设置输入 + onQuickSettingInput(e) { + const index = e.currentTarget.dataset.index + const value = e.detail.value + const quickSettings = [...this.data.quickSettings] + quickSettings[index] = value + this.setData({ quickSettings }) + }, + + // 添加快捷设置 + addQuickSetting() { + if (this.data.quickSettings.length >= 10) { + wx.showToast({ + title: '最多添加10个快捷设置', + icon: 'none' + }) + return + } + const quickSettings = [...this.data.quickSettings] + quickSettings.push('') + this.setData({ quickSettings }) + }, + + // 删除快捷设置 + removeQuickSetting(e) { + const index = e.currentTarget.dataset.index + if (this.data.quickSettings.length <= 1) { + wx.showToast({ + title: '至少保留一个快捷设置', + icon: 'none' + }) + return + } + const quickSettings = [...this.data.quickSettings] + quickSettings.splice(index, 1) + this.setData({ quickSettings }) + }, + + // 创建房间 + async createRoom() { + const roomName = this.data.roomName.trim() + const tableFee = parseFloat(this.data.tableFee) || 0 + const quickSettings = this.data.quickSettings + .map(val => parseInt(val)) + .filter(val => !isNaN(val) && val > 0) + + if (!roomName) { + wx.showToast({ + title: '请输入房间名称', + icon: 'none' + }) + return + } + + if (quickSettings.length === 0) { + wx.showToast({ + title: '请至少设置一个有效的快捷输入', + icon: 'none' + }) + return + } + + if (this.data.creating) return + + this.setData({ creating: true }) + wx.showLoading({ title: '创建中...' }) + + try { + const data = await request.post('/rooms/create', { + room_name: roomName, + game_type: this.data.gameType, + table_fee: tableFee, + quick_settings: quickSettings + }) + + wx.hideLoading() + + this.setData({ + inviteCode: data.invite_code, + roomCode: data.room_code, + roomId: data.room_id, + showSuccessModal: true + }) + + // 延迟生成二维码,确保canvas已渲染 + setTimeout(() => { + this.generateQRCode(data.invite_code) + }, 100) + + // 更新全局房间信息 + const app = getApp() + app.setCurrentSession({ + id: data.room_id, + room_code: data.room_code, + room_name: roomName + }) + + } catch (error) { + wx.hideLoading() + wx.showToast({ + title: error.message || '创建失败', + icon: 'none' + }) + } finally { + this.setData({ creating: false }) + } + }, + + // 生成二维码 + async generateQRCode(inviteCode) { + try { + const app = getApp() + // 构建scene参数(使用邀请码) + const scene = `code=${inviteCode}` + + console.log('【创建页】开始生成二维码, scene:', scene, 'invite_code:', inviteCode) + + wx.showLoading({ title: '生成二维码...' }) + + // 调用app.js中的getQrcode方法获取二维码 + const qrcodeBuffer = await app.getQrcode(scene) + + console.log('【创建页】二维码数据返回:', qrcodeBuffer) + + wx.hideLoading() + + // 检查返回数据类型 + if (!qrcodeBuffer) { + throw new Error('二维码数据为空') + } + + // 将arraybuffer转换为base64 + const base64 = wx.arrayBufferToBase64(qrcodeBuffer) + const qrcodeUrl = `data:image/png;base64,${base64}` + + console.log('【创建页】base64长度:', base64.length) + + // 更新页面数据,显示二维码图片 + this.setData({ + qrcodeUrl: qrcodeUrl + }) + + } catch (error) { + wx.hideLoading() + console.error('【创建页】生成二维码失败:', error) + wx.showToast({ + title: error.message || '二维码生成失败', + icon: 'none' + }) + } + }, + + // 复制邀请码 + copyInviteCode() { + if (!this.data.inviteCode) return + + wx.setClipboardData({ + data: this.data.inviteCode, + success: () => { + wx.showToast({ + title: '已复制邀请码', + icon: 'success' + }) + } + }) + }, + + // 进入房间 + goToRoom() { + if (!this.data.roomId) return + + wx.redirectTo({ + url: `/pages/game/detail/detail?id=${this.data.roomId}` + }) + }, + + // 关闭弹窗 + closeModal() { + this.setData({ + showSuccessModal: false + }) + }, + + // 阻止事件冒泡 + stopPropagation() { + // 阻止点击弹窗内容时关闭 + }, + + // 分享给朋友 + onShareAppMessage() { + if (this.data.inviteCode) { + return { + title: `邀请你加入房间:${this.data.roomName}`, + path: `/pages/game/join/join?code=${this.data.inviteCode}`, + imageUrl: '/images/share-bg.png' + } + } + } +}) diff --git a/pages/game/create/create.json b/pages/game/create/create.json new file mode 100644 index 0000000..c202186 --- /dev/null +++ b/pages/game/create/create.json @@ -0,0 +1,6 @@ +{ + "navigationStyle": "custom", + "usingComponents": { + "custom-navbar": "../../../components/navbar/navbar" + } +} \ No newline at end of file diff --git a/pages/game/create/create.wxml b/pages/game/create/create.wxml new file mode 100644 index 0000000..8208d30 --- /dev/null +++ b/pages/game/create/create.wxml @@ -0,0 +1,87 @@ + + + + + + + 房间名称 + + + + + + 游戏类型 + + + {{item.label}} + + + + + + + 桌位费 + + + + + + + 快捷输入设置 + (进入房间的所有用户将使用此设置) + + + + + × + + + + + 添加 + + + + + + + + + + + + + + 创建成功! + + + + + 房间号 + {{roomCode}} + + + + + + 生成中... + 邀请码:{{inviteCode}} + + + + + + + + + + \ No newline at end of file diff --git a/pages/game/create/create.wxss b/pages/game/create/create.wxss new file mode 100644 index 0000000..c8b8387 --- /dev/null +++ b/pages/game/create/create.wxss @@ -0,0 +1,368 @@ +/* 创建房间页面样式 */ +page { + background: linear-gradient(135deg, #1A1A1A 0%, #252525 100%); + min-height: 100vh; +} + +.container { + padding: 30rpx; + padding-bottom: 120rpx; +} + +/* 表单卡片 */ +.form-card { + background: #1A1A1Afff; + border-radius: 20rpx; + padding: 32rpx 24rpx; + margin-bottom: 30rpx; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.15); +} + +.form-item { + margin-bottom: 32rpx; +} + +.form-item:last-child { + margin-bottom: 0; +} + +.label { + display: block; + font-size: 28rpx; + color: #FFFFFF; + margin-bottom: 16rpx; + font-weight: 600; +} + +.label-row { + display: flex; + align-items: center; + margin-bottom: 16rpx; +} + +.label-hint { + font-size: 22rpx; + color: #B0B0B0; + margin-left: 8rpx; + font-weight: normal; +} + +.input { + width: 100%; + height: 80rpx; + background: #f8f9fa; + border: 2rpx solid rgba(0, 0, 0, 0.1); + border-radius: 12rpx; + padding-left: 24rpx; + font-size: 30rpx; + color: #FFFFFF; + box-sizing: border-box; + transition: all 0.3s; +} + +.input:focus { + border-color: #1A1A1A; + background: #1A1A1A; + box-shadow: 0 0 0 4rpx rgba(0, 0, 0, 0.1); +} + +/* 游戏类型选择器 */ +.game-type-selector { + display: flex; + gap: 12rpx; +} + +.type-option { + flex: 1; + height: 80rpx; + display: flex; + align-items: center; + justify-content: center; + background: #f8f9fa; + border: 2rpx solid rgba(0, 0, 0, 0.1); + border-radius: 12rpx; + font-size: 28rpx; + color: #E0E0E0; + transition: all 0.3s; + font-weight: 500; +} + +.type-option.active { + background: linear-gradient(135deg, #1A1A1A 0%, #252525 100%); + color: white; + border-color: #1A1A1A; + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.3); +} + +.type-option:active { + transform: scale(0.96); +} + +/* 快捷设置列表 */ +.quick-settings-list { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 12rpx; +} + +.quick-setting-item { + position: relative; + height: 80rpx; +} + +.quick-input { + width: 100%; + height: 100%; + background: #f8f9fa; + border: 2rpx solid rgba(0, 0, 0, 0.1); + border-radius: 12rpx; + padding: 0 10rpx; + font-size: 28rpx; + color: #FFFFFF; + box-sizing: border-box; + text-align: center; +} + +.quick-input:focus { + border-color: #1A1A1A; + background: #1A1A1A; + box-shadow: 0 0 0 4rpx rgba(0, 0, 0, 0.1); +} + +.remove-btn { + position: absolute; + top: -8rpx; + right: -8rpx; + width: 40rpx; + height: 40rpx; + background: #ff3b30; + color: white; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 28rpx; + line-height: 1; + box-shadow: 0 2rpx 8rpx rgba(255, 59, 48, 0.3); + z-index: 1; +} + +.remove-btn:active { + opacity: 0.8; + transform: scale(0.95); +} + +.add-btn { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + gap: 8rpx; + height: 80rpx; + background: rgba(0, 0, 0, 0.05); + border: 2rpx dashed rgba(0, 0, 0, 0.3); + border-radius: 12rpx; + color: #1A1A1A; + font-size: 24rpx; +} + +.add-btn:active { + background: rgba(0, 0, 0, 0.1); +} + +.add-icon { + font-size: 36rpx; + font-weight: bold; + line-height: 1; +} + +.add-text { + font-weight: 500; + font-size: 24rpx; +} + +/* 提交按钮 */ +.submit-btn { + width: calc(100% - 40rpx); + margin: 0 20rpx; + padding: 28rpx; + background: linear-gradient(135deg, #50C878 0%, #06a854 100%); + color: white; + border-radius: 16rpx; + font-size: 32rpx; + font-weight: bold; + border: none; + box-shadow: 0 8rpx 24rpx rgba(7, 193, 96, 0.4); + transition: all 0.3s; +} + +.submit-btn:active { + opacity: 0.9; + transform: scale(0.98); +} + +.submit-btn[disabled] { + background: #e0e0e0; + color: #B0B0B0; + box-shadow: none; +} + +/* 弹窗遮罩 */ +.modal-mask { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.7); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; +} + +/* 弹窗内容 */ +.modal-content { + width: 600rpx; + background: #1A1A1Afff; + border-radius: 24rpx; + overflow: hidden; +} + +/* 弹窗头部 */ +.modal-header { + padding: 48rpx 32rpx 32rpx; + text-align: center; + background: linear-gradient(135deg, #f0fdf4 0%, #e6f7ed 100%); +} + +.success-icon { + width: 100rpx; + height: 100rpx; + background: linear-gradient(135deg, #50C878 0%, #06a854 100%); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 64rpx; + color: white; + margin: 0 auto 20rpx; + line-height: 1; +} + +.success-title { + font-size: 36rpx; + font-weight: bold; + color: #FFFFFF; + display: block; +} + +/* 弹窗主体 */ +.modal-body { + padding: 32rpx; +} + +/* 编码区域 */ +.code-section { + background: #f8f9fa; + border-radius: 12rpx; + padding: 20rpx; + margin-bottom: 24rpx; + text-align: center; +} + +.code-label { + font-size: 24rpx; + color: #B0B0B0; + display: block; + margin-bottom: 8rpx; +} + +.code-value { + font-size: 40rpx; + font-weight: bold; + color: #FFFFFF; + letter-spacing: 4rpx; + display: block; + font-family: 'Courier New', monospace; +} + +/* 二维码区域 */ +.qrcode-section { + text-align: center; + position: relative; +} + +.qrcode-image { + width: 400rpx; + height: 400rpx; + margin: 0 auto 16rpx; + display: block; + border-radius: 8rpx; +} + +.qrcode-loading { + width: 400rpx; + height: 400rpx; + margin: 0 auto 16rpx; + background: #f5f5f5; + border-radius: 8rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 28rpx; + color: #B0B0B0; +} + +.invite-code { + font-size: 28rpx; + color: #E0E0E0; + display: block; + margin-bottom: 16rpx; +} + +.copy-btn { + width: 200rpx; + padding: 16rpx; + background: #f8f8f8; + color: #FFFFFF; + border-radius: 8rpx; + font-size: 28rpx; + border: none; + margin: 0 auto; +} + +.copy-btn:active { + background: #e8e8e8; +} + +/* 弹窗底部 */ +.modal-footer { + display: flex; + border-top: 1rpx solid #e5e5e5; +} + +.footer-btn { + flex: 1; + padding: 32rpx; + font-size: 32rpx; + font-weight: 500; + border: none; + background: #1A1A1A; + border-radius: 0; + margin: 0; +} + +.footer-btn:active { + background: #f8f8f8; +} + +.footer-btn.share-btn { + color: #ff9500; + border-right: 1rpx solid #e5e5e5; +} + +.footer-btn.enter-btn { + color: #50C878; + font-weight: bold; +} diff --git a/pages/game/detail/detail.js b/pages/game/detail/detail.js new file mode 100644 index 0000000..5f03875 --- /dev/null +++ b/pages/game/detail/detail.js @@ -0,0 +1,1038 @@ +// pages/game/detail/detail.js +import request from '../../../utils/request' +import { formatTime } from '../../../utils/format' + +// 格式化积分显示(超过1000显示k) +function formatScore(score) { + const num = parseFloat(score) || 0 + if (Math.abs(num) >= 1000) { + return (num / 1000).toFixed(1) + 'k' + } + return num.toString() +} + +Page({ + data: { + sessionId: null, + session: null, + players: [], + records: [], + logs: [], + logsScrollTop: 0, + hasMoreLogs: false, + logsPage: 1, + userInfo: null, + isHost: false, + isInSession: false, + totalRounds: 0, + showShareModal: false, + showPlayerModal: false, + showExpenseModal: false, + showSettingsModal: false, + selectedPlayer: null, + scoreInput: '', + otherPlayers: [], + expenseInputs: {}, + tableFee: 0, + quickInputs: [50, 100, 200, 500], + ttsEnabled: true, // 语音播报开关,默认开启 + refreshTimer: null, + navTitle: '房间详情', + announcement: '', + navbarHeight: 0, // 导航栏高度 + qrcodeUrl: '', // 二维码URL + hasShownRejoinPrompt: false, // 标记是否已显示过重新加入提示 + + statusText: { + 'waiting': '等待中', + 'playing': '游戏中', + 'paused': '已暂停', + 'finished': '已结束' + }, + + gameTypeText: { + 'mahjong': '麻将', + 'poker': '扑克', + 'other': '其他' + } + }, + + onLoad(options) { + // 计算导航栏高度 + const windowInfo = wx.getWindowInfo() + const menuButtonInfo = wx.getMenuButtonBoundingClientRect() + const statusBarHeight = windowInfo.statusBarHeight + const menuButtonTop = menuButtonInfo.top + const navbarHeight = menuButtonInfo.bottom + (menuButtonTop - statusBarHeight) + this.setData({ navbarHeight }) + + if (!options.id) { + wx.showToast({ + title: '牌局不存在', + icon: 'none' + }) + setTimeout(() => { + wx.navigateBack() + }, 1500) + return + } + console.log(options.id) + this.setData({ + sessionId: options.id + }) + // 获取用户信息 + const app = getApp() + const userInfo = app.getUserInfo() + if (!userInfo) { + wx.showToast({ + title: '请先登录', + icon: 'none' + }) + setTimeout(() => { + wx.navigateBack() + }, 1500) + return + } + + this.setData({ userInfo }) + + // 加载TTS设置 + const ttsEnabled = wx.getStorageSync('ttsEnabled') + if (ttsEnabled !== '') { + this.setData({ ttsEnabled: ttsEnabled }) + } + + // 加载公告 + this.loadAnnouncements() + + // 加载牌局详情(包含日志) + this.loadSessionDetail() + + // 设置自动刷新 + this.startAutoRefresh() + + }, + + onShow() { + if (this.data.sessionId) { + this.loadSessionDetail() + // 页面重新显示时,重启自动刷新 + this.startAutoRefresh() + } + }, + + onHide() { + // 页面隐藏时清除定时器,避免后台继续请求 + this.stopAutoRefresh() + }, + + onUnload() { + // 页面卸载时清除定时器 + this.stopAutoRefresh() + }, + + // 停止自动刷新 + stopAutoRefresh() { + if (this.data.refreshTimer) { + clearInterval(this.data.refreshTimer) + this.setData({ refreshTimer: null }) + } + }, + + // 启动自动刷新 + startAutoRefresh() { + // 如果已有定时器,先清除 + this.stopAutoRefresh() + + // 每5秒刷新一次 + const timer = setInterval(() => { + if (this.data.session) { + this.loadSessionDetail(true) + } + }, 5000) + + this.setData({ refreshTimer: timer }) + }, + + // 加载公告 + async loadAnnouncements() { + try { + const data = await request.get('/announcements') + if (data && data.length > 0) { + // 将最新的2条公告用"|"连接 + const announcementText = data.map(item => item.content).join(' | ') + this.setData({ announcement: announcementText }) + } + } catch (error) { + console.error('加载公告失败:', error) + // 使用默认公告 + this.setData({ announcement: '欢迎来到打牌记账,祝大家游戏愉快!' }) + } + }, + + // 加载牌局实时数据(统一接口) + async loadSessionDetail(silent = false) { + if (!silent) { + wx.showLoading({ title: '加载中...' }) + } + + try { + // 调用统一的实时数据接口 + const realtimeData = await request.get(`/rooms/${this.data.sessionId}/realtime`) + + const { room, players, records, logs, user_info } = realtimeData + + // 检测用户是否曾经在房间但已离开(仅在初次加载时,不是自动刷新,并且未显示过提示) + if (!silent && user_info.has_left && !this.data.hasShownRejoinPrompt) { + if (!silent) { + wx.hideLoading() + } + + // 标记已显示过提示,避免重复弹窗 + this.setData({ hasShownRejoinPrompt: true }) + + // 停止自动刷新 + this.stopAutoRefresh() + + wx.showModal({ + title: '检测到您已离开此房间', + content: '您之前已离开过此房间,是否重新加入?', + confirmText: '重新加入', + cancelText: '返回首页', + success: async (res) => { + if (res.confirm) { + // 重新加入房间 + try { + wx.showLoading({ title: '加入中...' }) + await request.post('/rooms/join', { + room_code: room.room_code + }) + wx.hideLoading() + wx.showToast({ + title: '重新加入成功', + icon: 'success' + }) + // 刷新页面并重启自动刷新 + this.loadSessionDetail() + this.startAutoRefresh() + } catch (error) { + wx.hideLoading() + wx.showToast({ + title: error.message || '加入失败', + icon: 'none' + }) + // 返回首页 + setTimeout(() => { + wx.switchTab({ + url: '/pages/index/index' + }) + }, 1500) + } + } else { + // 返回首页(tabBar页面使用switchTab) + wx.switchTab({ + url: '/pages/index/index' + }) + } + } + }) + return + } + + // 检测房间状态变化(仅在自动刷新时) + if (silent && this.data.session) { + const oldStatus = this.data.session.status + const newStatus = room.status + const oldIsInSession = this.data.isInSession + const newIsInSession = user_info.is_in_session + + // 1. 检测房间是否已关闭 + if (oldStatus === 'playing' && newStatus === 'finished') { + // 停止自动刷新 + this.stopAutoRefresh() + + wx.showModal({ + title: '牌局已结束', + content: '牌局已经结束,是否查看战绩?', + confirmText: '查看战绩', + cancelText: '退出', + success: (res) => { + if (res.confirm) { + // 跳转到统计页(tabBar页面使用switchTab) + wx.switchTab({ + url: '/pages/stats/personal/personal' + }) + } else { + // 返回首页(tabBar页面使用switchTab) + wx.switchTab({ + url: '/pages/index/index' + }) + } + } + }) + return + } + + // 2. 检测用户是否离开了房间 + if (oldIsInSession && !newIsInSession) { + // 停止自动刷新 + this.stopAutoRefresh() + + wx.showModal({ + title: '已离开房间', + content: '您已离开此牌局,是否重新加入?', + confirmText: '重新加入', + cancelText: '返回首页', + success: async (res) => { + if (res.confirm) { + // 重新加入房间 + try { + wx.showLoading({ title: '加入中...' }) + await request.post('/rooms/join', { + room_code: this.data.session.room_code + }) + wx.hideLoading() + wx.showToast({ + title: '重新加入成功', + icon: 'success' + }) + // 刷新页面并重启自动刷新 + this.loadSessionDetail() + this.startAutoRefresh() + } catch (error) { + wx.hideLoading() + wx.showToast({ + title: error.message || '加入失败', + icon: 'none' + }) + // 返回首页 + setTimeout(() => { + wx.redirectTo({ + url: '/pages/index/index' + }) + }, 1500) + } + } else { + // 返回首页(tabBar页面使用switchTab) + wx.switchTab({ + url: '/pages/index/index' + }) + } + } + }) + return + } + } + + // 格式化创建时间 + const createTime = formatTime(new Date(room.created_at * 1000)) + + // 格式化战绩时间 + const formattedRecords = (records || []).map(record => { + const time = new Date(record.created_at * 1000) + return { + ...record, + timeText: `${time.getMonth() + 1}/${time.getDate()} ${time.getHours()}:${String(time.getMinutes()).padStart(2, '0')}` + } + }) + + // 格式化日志时间 + const formattedLogs = (logs || []).map(log => { + const time = new Date(log.created_at * 1000) + const month = String(time.getMonth() + 1).padStart(2, '0') + const day = String(time.getDate()).padStart(2, '0') + const hours = String(time.getHours()).padStart(2, '0') + const minutes = String(time.getMinutes()).padStart(2, '0') + const seconds = String(time.getSeconds()).padStart(2, '0') + return { + ...log, + timeText: `${month}-${day} ${hours}:${minutes}:${seconds}` + } + }) + + // 查找房主昵称 + const hostPlayer = players.find(p => p.is_host) + //const hostNickname = hostPlayer ? hostPlayer.nickname : '房主' + const navTitle = `${room.room_name}` + + // 获取除自己外的其他玩家 + const otherPlayers = players.filter(p => p.player_id !== this.data.userInfo.id) + + // 更新快捷输入设置 + const quickInputs = room.quick_settings || [10, 20, 30] + + // 格式化玩家积分显示 + const formattedPlayers = players.map(p => ({ + ...p, + formatted_score: formatScore(p.total_win_loss) + })) + + this.setData({ + session: room, + players: formattedPlayers, + records: formattedRecords, + logs: formattedLogs, + isInSession: user_info.is_in_session, + isHost: user_info.is_host, + createTime, + totalRounds: room.total_rounds || 0, + navTitle, + otherPlayers, + quickInputs, + tableFee: room.table_fee || 0 + }) + + if (!silent) { + wx.hideLoading() + } + + } catch (error) { + // 静默刷新时忽略错误(网络问题等),避免打扰用户 + if (!silent) { + wx.hideLoading() + wx.showToast({ + title: error.message || '加载失败', + icon: 'none' + }) + } else { + // 静默刷新失败时,仅在控制台记录错误,不显示给用户 + console.warn('自动刷新失败:', error) + } + } + }, + + // 开始游戏 + async startGame() { + if (this.data.players.length < 2) { + wx.showToast({ + title: '至少需要2人才能开始', + icon: 'none' + }) + return + } + + wx.showModal({ + title: '开始游戏', + content: `确定要开始游戏吗?当前${this.data.players.length}人`, + success: async (res) => { + if (res.confirm) { + wx.showLoading({ title: '启动中...' }) + + try { + await request.post(`/rooms/${this.data.sessionId}/start`) + + wx.hideLoading() + wx.showToast({ + title: '游戏已开始', + icon: 'success' + }) + + // 刷新页面 + this.loadSessionDetail() + + // 跳转到记账页 + setTimeout(() => { + this.goToPlay() + }, 1500) + + } catch (error) { + wx.hideLoading() + wx.showToast({ + title: error.message || '启动失败', + icon: 'none' + }) + } + } + } + }) + }, + + // 暂停游戏 + async pauseGame() { + wx.showModal({ + title: '暂停游戏', + content: '确定要暂停游戏吗?', + success: async (res) => { + if (res.confirm) { + try { + await request.post(`/rooms/${this.data.sessionId}/pause`) + + wx.showToast({ + title: '已暂停', + icon: 'success' + }) + + this.loadSessionDetail() + + } catch (error) { + wx.showToast({ + title: error.message || '操作失败', + icon: 'none' + }) + } + } + } + }) + }, + + // 结束牌局 + confirmEndSession() { + wx.showModal({ + title: '结束牌局', + content: '确定要结束牌局吗?结束后将无法继续记账', + confirmColor: '#ff4444', + success: async (res) => { + if (res.confirm) { + wx.showLoading({ title: '处理中...' }) + + try { + await request.post(`/rooms/${this.data.sessionId}/end`) + + wx.hideLoading() + wx.showToast({ + title: '牌局已结束', + icon: 'success' + }) + + this.loadSessionDetail() + + } catch (error) { + wx.hideLoading() + wx.showToast({ + title: error.message || '操作失败', + icon: 'none' + }) + } + } + } + }) + }, + + // 返回上一页(不离开房间) + confirmLeaveSession() { + wx.navigateBack() + }, + + // 加入牌局 + async joinSession() { + wx.showLoading({ title: '加入中...' }) + + try { + await request.post('/rooms/join', { + session_id: this.data.sessionId + }) + + wx.hideLoading() + wx.showToast({ + title: '加入成功', + icon: 'success' + }) + + // 刷新页面 + this.loadSessionDetail() + + } catch (error) { + wx.hideLoading() + wx.showToast({ + title: error.message || '加入失败', + icon: 'none' + }) + } + }, + + // 显示分享弹窗 + async shareSession() { + // 打开分享弹窗时暂停自动刷新 + this.stopAutoRefresh() + this.setData({ + showShareModal: true, + qrcodeUrl: '' // 重置二维码 + }) + + // 生成二维码 + await this.generateQRCode() + }, + + // 生成二维码 + async generateQRCode() { + if (!this.data.session || !this.data.session.invite_code) { + console.error('无法生成二维码: session或invite_code不存在', this.data.session) + wx.showToast({ + title: '房间信息不完整', + icon: 'none' + }) + return + } + + try { + const app = getApp() + const scene = `qrcode&code=${this.data.session.invite_code}` + + console.log('开始生成二维码, scene:', scene, 'invite_code:', this.data.session.invite_code) + + wx.showLoading({ title: '生成二维码...' }) + + const qrcodeBuffer = await app.getQrcode(scene) + + console.log('二维码数据返回:', qrcodeBuffer) + + wx.hideLoading() + + // 检查返回数据类型 + if (!qrcodeBuffer) { + throw new Error('二维码数据为空') + } + + // 将arraybuffer转换为base64 + const base64 = wx.arrayBufferToBase64(qrcodeBuffer) + const qrcodeUrl = `data:image/png;base64,${base64}` + + console.log('base64长度:', base64.length) + + this.setData({ + qrcodeUrl: qrcodeUrl + }) + + } catch (error) { + wx.hideLoading() + console.error('生成二维码失败:', error) + wx.showToast({ + title: error.message || '二维码生成失败', + icon: 'none' + }) + } + }, + + // 关闭分享弹窗 + closeShareModal() { + this.setData({ + showShareModal: false + }) + // 关闭分享弹窗后恢复自动刷新 + this.startAutoRefresh() + }, + + // 复制邀请码 + copyInviteCode() { + wx.setClipboardData({ + data: this.data.session.invite_code, + success: () => { + wx.showToast({ + title: '已复制邀请码', + icon: 'success' + }) + } + }) + }, + + // 进入记账页 + goToPlay() { + wx.navigateTo({ + url: `/pages/game/settlement/settlement?id=${this.data.sessionId}` + }) + }, + + // 查看统计 + goToStats() { + wx.navigateTo({ + url: `/pages/stats/session/session?id=${this.data.sessionId}` + }) + }, + + // 查看所有战绩 + goToRecords() { + wx.navigateTo({ + url: `/pages/game/records/records?id=${this.data.sessionId}` + }) + }, + + // 再来一局 + createNewSession() { + wx.showModal({ + title: '再来一局', + content: '是否使用相同设置创建新牌局?', + success: (res) => { + if (res.confirm) { + wx.navigateTo({ + url: `/pages/game/create/create?copy=${this.data.sessionId}` + }) + } + } + }) + }, + + // 下拉刷新 + onPullDownRefresh() { + this.loadSessionDetail().then(() => { + wx.stopPullDownRefresh() + }) + }, + + // 点击玩家卡片 + onPlayerTap(e) { + const player = e.currentTarget.dataset.player + // 打开玩家弹窗时暂停自动刷新 + this.stopAutoRefresh() + this.setData({ + selectedPlayer: player, + showPlayerModal: true, + scoreInput: '' + }) + }, + + // 关闭玩家弹窗 + closePlayerModal() { + this.setData({ + showPlayerModal: false, + selectedPlayer: null, + scoreInput: '' + }) + // 关闭玩家弹窗后恢复自动刷新 + this.startAutoRefresh() + }, + + // 分数输入 + onScoreInput(e) { + this.setData({ + scoreInput: e.detail.value + }) + }, + + // 快捷输入 + quickInput(e) { + const value = e.currentTarget.dataset.value + this.setData({ + scoreInput: value + }) + }, + + // 提交分数 + async submitScore() { + const score = parseFloat(this.data.scoreInput) + + // 验证金额格式(正数,最多2位小数) + if (isNaN(score) || score <= 0) { + wx.showToast({ + title: '请输入有效金额', + icon: 'none' + }) + return + } + + // 验证最大值99999 + if (score > 99999) { + wx.showToast({ + title: '金额不能超过99999', + icon: 'none' + }) + return + } + + // 验证最多2位小数 + if (!/^\d+(\.\d{1,2})?$/.test(this.data.scoreInput)) { + wx.showToast({ + title: '金额最多保留2位小数', + icon: 'none' + }) + return + } + + wx.showLoading({ title: '提交中...' }) + + try { + // 调用记分转账API(当前用户输给选中的玩家) + await request.post('/records/transfer', { + room_id: this.data.sessionId, + to_player_id: this.data.selectedPlayer.player_id, + amount: score + }) + + wx.hideLoading() + wx.showToast({ + title: '记分成功', + icon: 'success' + }) + + // 播放TTS语音 + const ttsText = `${this.data.userInfo.nickname}付给${this.data.selectedPlayer.nickname}${score}元` + this.playTTS(ttsText) + + this.closePlayerModal() + this.loadSessionDetail() + + } catch (error) { + wx.hideLoading() + wx.showToast({ + title: error.message || '提交失败', + icon: 'none' + }) + } + }, + + + // 显示支出弹窗 + showExpenseModal() { + // 打开支出弹窗时暂停自动刷新 + this.stopAutoRefresh() + this.setData({ + showExpenseModal: true, + expenseInputs: {} + }) + }, + + // 关闭支出弹窗 + closeExpenseModal() { + this.setData({ + showExpenseModal: false, + expenseInputs: {} + }) + // 关闭支出弹窗后恢复自动刷新 + this.startAutoRefresh() + }, + + // 支出金额输入 + onExpenseInput(e) { + const index = e.currentTarget.dataset.index + const value = e.detail.value + const expenseInputs = {...this.data.expenseInputs} + expenseInputs[index] = value + this.setData({ expenseInputs }) + }, + + // 提交支出 + async submitExpense() { + const expenses = [] + Object.keys(this.data.expenseInputs).forEach(index => { + const amount = parseFloat(this.data.expenseInputs[index]) + if (!isNaN(amount) && amount > 0) { + // 验证最大值99999 + if (amount > 99999) { + wx.showToast({ + title: '金额不能超过99999', + icon: 'none' + }) + return + } + + // 验证最多2位小数 + if (!/^\d+(\.\d{1,2})?$/.test(this.data.expenseInputs[index])) { + wx.showToast({ + title: '金额最多保留2位小数', + icon: 'none' + }) + return + } + + expenses.push({ + playerId: this.data.otherPlayers[index].player_id, + amount: amount + }) + } + }) + + if (expenses.length === 0) { + wx.showToast({ + title: '请至少输入一项支出', + icon: 'none' + }) + return + } + + wx.showLoading({ title: '提交中...' }) + + try { + // 串行调用记分转账API(避免数据库死锁) + let successCount = 0 + const successExpenses = [] + for (const expense of expenses) { + try { + await request.post('/records/transfer', { + room_id: this.data.sessionId, + to_player_id: expense.playerId, + amount: expense.amount + }) + successCount++ + successExpenses.push(expense) + } catch (error) { + console.error('记分失败:', expense, error) + // 继续执行其他记分,不中断 + } + } + + wx.hideLoading() + + if (successCount === expenses.length) { + wx.showToast({ + title: `成功记录${successCount}笔支出`, + icon: 'success' + }) + + // 播放批量支出语音 + if (successExpenses.length > 0) { + const ttsTexts = successExpenses.map(exp => { + const player = this.data.otherPlayers.find(p => p.player_id === exp.playerId) + return `${this.data.userInfo.nickname}付给${player.nickname}${exp.amount}元` + }) + // 播放第一条(可以根据需要调整为播放所有或汇总) + this.playTTS(ttsTexts.join(',')) + } + } else if (successCount > 0) { + wx.showToast({ + title: `成功${successCount}笔,失败${expenses.length - successCount}笔`, + icon: 'none', + duration: 3000 + }) + + // 播放成功的语音 + if (successExpenses.length > 0) { + const ttsTexts = successExpenses.map(exp => { + const player = this.data.otherPlayers.find(p => p.player_id === exp.playerId) + return `${this.data.userInfo.nickname}付给${player.nickname}${exp.amount}元` + }) + this.playTTS(ttsTexts.join(',')) + } + } else { + wx.showToast({ + title: '所有记分都失败了', + icon: 'none' + }) + } + + this.closeExpenseModal() + this.loadSessionDetail() + + } catch (error) { + wx.hideLoading() + wx.showToast({ + title: error.message || '提交失败', + icon: 'none' + }) + } + }, + + // 显示设置弹窗 + showSettingsModal() { + // 打开设置弹窗时暂停自动刷新 + this.stopAutoRefresh() + this.setData({ + showSettingsModal: true + }) + }, + + // 关闭设置弹窗 + closeSettingsModal() { + this.setData({ + showSettingsModal: false + }) + // 关闭设置弹窗后恢复自动刷新 + this.startAutoRefresh() + }, + + // 桌位费输入 + onTableFeeInput(e) { + this.setData({ + tableFee: e.detail.value + }) + }, + + // 语音播报开关切换 + onTtsToggle(e) { + const ttsEnabled = e.detail.value + this.setData({ ttsEnabled }) + // 保存到本地存储 + wx.setStorageSync('ttsEnabled', ttsEnabled) + }, + + // TTS语音播放 + async playTTS(text) { + // 如果语音播报关闭,直接返回 + if (!this.data.ttsEnabled) { + return + } + + try { + const result = await request.post('/tts/convert', { text }) + + if (result.audioUrl) { + // 使用音频URL直接播放 + const innerAudioContext = wx.createInnerAudioContext() + innerAudioContext.src = `https://ca.miniappapi.com/mp${result.audioUrl}` + innerAudioContext.play() + + // 播放完成后销毁音频上下文 + innerAudioContext.onEnded(() => { + innerAudioContext.destroy() + }) + + // 播放失败处理 + innerAudioContext.onError((error) => { + console.error('音频播放失败:', error) + innerAudioContext.destroy() + }) + } + } catch (error) { + console.error('TTS转换失败:', error) + } + }, + + // 快捷输入值修改 + onQuickInputChange(e) { + const index = e.currentTarget.dataset.index + const value = e.detail.value + const quickInputs = [...this.data.quickInputs] + quickInputs[index] = value + this.setData({ quickInputs }) + }, + + // 删除快捷输入 + deleteQuickInput(e) { + const index = e.currentTarget.dataset.index + const quickInputs = [...this.data.quickInputs] + quickInputs.splice(index, 1) + this.setData({ quickInputs }) + }, + + // 添加快捷输入 + addQuickInput() { + const quickInputs = [...this.data.quickInputs] + quickInputs.push('') + this.setData({ quickInputs }) + }, + + // 保存设置 + async saveSettings() { + wx.showLoading({ title: '保存中...' }) + + try { + await request.put(`/rooms/${this.data.sessionId}/settings`, { + table_fee: parseFloat(this.data.tableFee) || 0, + quick_settings: this.data.quickInputs.map(v => parseFloat(v) || 0) + }) + + wx.hideLoading() + wx.showToast({ + title: '保存成功', + icon: 'success' + }) + + this.closeSettingsModal() + this.loadSessionDetail(true) + + } catch (error) { + wx.hideLoading() + wx.showToast({ + title: error.message || '保存失败', + icon: 'none' + }) + } + }, + + // 分享给朋友 + onShareAppMessage() { + return { + title: `邀请你加入牌局:${this.data.session.room_name}`, + path: `/pages/game/join/join?code=${this.data.session.invite_code}`, + imageUrl: '/images/share-bg.png' + } + } +}) \ No newline at end of file diff --git a/pages/game/detail/detail.json b/pages/game/detail/detail.json new file mode 100644 index 0000000..4c7816e --- /dev/null +++ b/pages/game/detail/detail.json @@ -0,0 +1 @@ +{"navigationStyle":"custom","usingComponents":{"custom-navbar":"../../../components/navbar/navbar", "jdwx-ad": "@jdmini/components/jdwx-ad"}} diff --git a/pages/game/detail/detail.wxml b/pages/game/detail/detail.wxml new file mode 100644 index 0000000..369b103 --- /dev/null +++ b/pages/game/detail/detail.wxml @@ -0,0 +1,274 @@ + + + + + + 📢 + + + {{announcement || '欢迎来到打牌记账,祝大家游戏愉快!'}} + + + + + + + + + + + 玩家 ({{players.length}}人) + 已进行 {{totalRounds}} 局 + + + + + + + + {{item.nickname}} + + {{item.total_win_loss > 0 ? '+' : ''}}{{item.formatted_score}} + + + + + + {{item.nickname}} + + {{item.total_win_loss > 0 ? '+' : ''}}{{item.formatted_score}} + + 房主 + + + + + + + + + + + + 邀请 + + + + + + + + 记分记录 + 共{{logs.length}}条 + + + + + + {{item.timeText}} + + + {{item.from_nickname}} + 付给 + {{item.to_nickname}} + ¥{{item.amount}} + + + + {{item.player_nickname}} + 进入了房间 + + + + {{item.player_nickname}} + 退出了房间 + + + + {{item.player_nickname}} + 设置了桌位费 + ¥{{item.amount}} + + + + + 暂无操作记录 + + + + 加载更多 + + + + + + + + + + + + + 💰 + 支出 + + + ⚙️ + 设置 + + + + + + + + + + + + 玩家信息 + × + + + + + + + {{selectedPlayer.nickname}} + + 当前积分: {{selectedPlayer.total_win_loss > 0 ? '+' : ''}}{{selectedPlayer.total_win_loss || 0}} + + + + + + + + + + + + 记分: + + + + + +{{item}} + + + + + + + + + + + + + + + 记录支出 + × + + + + + + + + {{item.nickname}} + + + + + + + + + + + + + + + + 设置 + × + + + + + + + 语音播报 + + + + + + + 桌位费 + + + + + + + 快捷输入设置 + (所有玩家使用此设置) + + + + + × + + + + + 添加 + + + + + + + + + + + \ No newline at end of file diff --git a/pages/game/detail/detail.wxss b/pages/game/detail/detail.wxss new file mode 100644 index 0000000..2f76037 --- /dev/null +++ b/pages/game/detail/detail.wxss @@ -0,0 +1,873 @@ +/* 牌局详情页面样式 */ +page { + background: linear-gradient(135deg, #1A1A1A 0%, #252525 100%); + min-height: 100vh; +} + +/* ========== 跑马灯公告条 ========== */ +.marquee-container { + background: rgba(255, 255, 255, 0.95); + padding: 16rpx 24rpx; + display: flex; + align-items: center; + gap: 16rpx; + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05); +} + +.marquee-icon { + font-size: 32rpx; + flex-shrink: 0; +} + +.marquee-content { + flex: 1; + overflow: hidden; + position: relative; + height: 40rpx; + line-height: 40rpx; +} + +.marquee-text { + white-space: nowrap; + font-size: 26rpx; + color: #E0E0E0; + display: inline-block; + padding-left: 100%; + animation: marquee 20s linear infinite; +} + +@keyframes marquee { + 0% { + transform: translateX(0); + } + 100% { + transform: translateX(-100%); + } +} + +/* ========== 容器 ========== */ +.container { + padding: 20rpx 30rpx 140rpx; +} + +/* ========== 玩家卡片区域 ========== */ +.players-section { + margin-bottom: 24rpx; +} + +.section-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 0 20rpx; +} + +.section-title { + font-size: 34rpx; + font-weight: bold; + color: white; + text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1); +} + +.total-rounds { + font-size: 26rpx; + color: rgba(255, 255, 255, 0.9); + background: rgba(255, 255, 255, 0.15); + padding: 6rpx 16rpx; + border-radius: 20rpx; + backdrop-filter: blur(10rpx); +} + +.players-scroll { + white-space: nowrap; + height: 220rpx; +} + +.player-card { + display: inline-block; + vertical-align: top; + width: 120rpx; + height: 180rpx; + background: #1A1A1A; + border-radius: 20rpx; + padding: 20rpx 16rpx 16rpx; + margin-right: 16rpx; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.2); + position: relative; + transition: all 0.3s; + text-align: center; +} + +.player-card:active { + transform: translateY(-4rpx); + box-shadow: 0 8rpx 28rpx rgba(0, 0, 0, 0.3); +} + +.my-card { + background: linear-gradient(135deg, #1A1A1A 0%, #252525 100%); + border: 2rpx solid rgba(255, 255, 255, 0.3); +} + +.my-card .player-card-nickname, +.my-card .player-card-score { + color: white; +} + +.host-card { + border: 2rpx solid rgba(255, 154, 118, 0.5); +} + +.invite-card { + background: rgba(255, 255, 255, 0.2); + border: 2rpx dashed rgba(255, 255, 255, 0.5); + backdrop-filter: blur(10rpx); + display: inline-flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.invite-card:active { + background: rgba(255, 255, 255, 0.3); +} + +.player-card-avatar { + width: 70rpx; + height: 70rpx; + border-radius: 50%; + margin: 0 auto 12rpx; + border: 3rpx solid rgba(0, 0, 0, 0.1); + display: block; +} + +.my-card .player-card-avatar { + border-color: rgba(255, 255, 255, 0.5); +} + +.player-card-nickname { + font-size: 24rpx; + color: #FFFFFF; + font-weight: 500; + margin: 0 auto 8rpx; + max-width: 110rpx; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: block; +} + +.player-card-score { + font-size: 30rpx; + font-weight: bold; + color: #FFFFFF; + display: block; +} + +.player-card-score.win { + color: #50C878; +} + +.player-card-score.lose { + color: #ff3b30; +} + +.player-card-tag { + position: absolute; + top: 8rpx; + right: 8rpx; + font-size: 20rpx; + padding: 4rpx 10rpx; + border-radius: 20rpx; + font-weight: 500; + line-height: 1; +} + +.me-tag { + background: rgba(255, 255, 255, 0.3); + color: white; +} + +.host-tag { + background: rgba(255, 154, 118, 0.15); + color: #ff9a76; +} + +.invite-icon { + width: 70rpx; + height: 70rpx; + border-radius: 50%; + background: rgba(255, 255, 255, 0.3); + display: flex; + align-items: center; + justify-content: center; + font-size: 44rpx; + color: white; + margin-bottom: 8rpx; +} + +.invite-text { + font-size: 24rpx; + color: white; + font-weight: 500; +} + +/* ========== 操作日志区域 ========== */ +.logs-section { + margin-bottom: 30rpx; +} + +.log-count { + font-size: 26rpx; + color: rgba(255, 255, 255, 0.9); + background: rgba(255, 255, 255, 0.15); + padding: 6rpx 16rpx; + border-radius: 20rpx; + backdrop-filter: blur(10rpx); +} + +.logs-card { + background: #1A1A1A; + border-radius: 20rpx; + padding: 0; + overflow: hidden; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.2); +} + +.logs-scroll { + height: 500rpx; + padding: 24rpx; +} + +.log-item { + display: flex; + flex-direction: column; + margin-bottom: 24rpx; + padding-bottom: 24rpx; + border-bottom: 1rpx solid #f5f5f5; +} + +.log-item:last-child { + border-bottom: none; + margin-bottom: 0; + padding-bottom: 0; +} + +.log-time { + font-size: 22rpx; + line-height: 22rpx; + color: #B0B0B0; + text-align: center; + margin-bottom: 8rpx; +} + +.log-text { + font-size: 28rpx; + color: #FFFFFF; + line-height: 1.5; + display: flex; + flex-wrap: wrap; + align-items: baseline; + text-align: left; +} + +/* 玩家昵称样式 */ +.log-player-name { + padding:0 10rpx 0 10rpx; + color: #1A1A1A; + font-weight: 600; +} + +.log-player-name.self { + color: #50C878; /* 微信绿色,表示自己 */ +} + +/* 操作描述样式 */ +.log-action { + color: #E0E0E0; +} + +/* 金额样式 */ +.log-amount { + color: #ff6b6b; + font-weight: 700; + font-size: 30rpx; +} + +.log-detail { + font-size: 24rpx; + color: #1A1A1A; + font-weight: 600; +} + +.logs-empty { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 80rpx 0; +} + +.empty-text { + font-size: 28rpx; + color: #ccc; +} + +.logs-loadmore { + padding: 24rpx; + text-align: center; + font-size: 26rpx; + color: #1A1A1A; + border-top: 1rpx solid #f5f5f5; + margin-top: 16rpx; + cursor: pointer; +} + +.logs-loadmore:active { + background: rgba(0, 0, 0, 0.05); +} + +/* ========== 底部固定操作栏 ========== */ +.bottom-bar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background: #1A1A1A; + padding: 16rpx 30rpx; + box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.1); + z-index: 100; +} + +.bottom-actions { + display: flex; + gap: 16rpx; + align-items: stretch; +} + +.action-group { + display: flex; + gap: 16rpx; +} + +.action-btn { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background: #f8f9fa; + border-radius: 16rpx; + padding: 16rpx 20rpx; + min-width: 100rpx; + transition: all 0.3s; +} + +.action-btn:active { + background: rgba(0, 0, 0, 0.1); + transform: scale(0.95); +} + +.action-icon { + font-size: 32rpx; + margin-bottom: 4rpx; +} + +.action-label { + font-size: 22rpx; + color: #FFFFFF; + font-weight: 500; +} + +.settle-btn { + flex: 1; + background: linear-gradient(135deg, #1A1A1A 0%, #252525 100%); + color: white; + border-radius: 16rpx; + padding: 24rpx; + font-size: 32rpx; + font-weight: bold; + border: none; + box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.4); + transition: all 0.3s; + display: flex; + align-items: center; + justify-content: center; +} + +.settle-btn:active { + opacity: 0.9; + transform: scale(0.98); +} + +/* ========== 弹窗通用样式 ========== */ +.modal-mask { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 999; +} + +.modal-content { + position: fixed; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + width: 620rpx; + max-height: 80vh; + background: #1A1A1A; + border-radius: 24rpx; + overflow: hidden; + z-index: 1000; +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 32rpx 32rpx 24rpx; + border-bottom: 1rpx solid #f0f0f0; +} + +.modal-title { + font-size: 34rpx; + font-weight: bold; + color: #FFFFFF; +} + +.close-btn { + font-size: 48rpx; + color: #B0B0B0; + line-height: 1; + width: 48rpx; + height: 48rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.modal-body { + padding: 32rpx; + max-height: 60vh; + overflow-y: auto; +} + +/* ========== 玩家操作弹窗 ========== */ +.player-modal-content { + padding: 0; +} + +.player-modal-info { + display: flex; + flex-direction: column; + align-items: center; + padding: 20rpx 20rpx 20rpx; + background: linear-gradient(135deg, rgba(0, 0, 0, 0.05) 0%, rgba(118, 75, 162, 0.05) 100%); + border-bottom: 1rpx solid #f0f0f0; +} + +.modal-avatar { + width: 120rpx; + height: 120rpx; + border-radius: 50%; + margin-bottom: 16rpx; + border: 4rpx solid rgba(0, 0, 0, 0.2); +} + +.modal-nickname { + font-size: 36rpx; + font-weight: bold; + color: #FFFFFF; + margin-bottom: 12rpx; +} + +.modal-score { + font-size: 40rpx; + font-weight: bold; + color: #FFFFFF; +} + +.modal-score.win { + color: #50C878; +} + +.modal-score.lose { + color: #ff3b30; +} + +.score-input-section { + padding: 32rpx; +} + +.input-label { + display: block; + font-size: 28rpx; + font-weight: 600; + color: #FFFFFF; + margin-bottom: 16rpx; +} + +.score-input { + width: 100%; + background: #f8f9fa; + border: 2rpx solid rgba(0, 0, 0, 0.1); + border-radius: 12rpx; + height: 60rpx; + padding-left: 24rpx; + font-size: 32rpx; + color: #FFFFFF; + text-align: center; + font-weight: bold; + box-sizing: border-box; + margin-bottom: 24rpx; +} + +.score-input:focus { + border-color: #1A1A1A; + background: #1A1A1A; + box-shadow: 0 0 0 4rpx rgba(0, 0, 0, 0.1); +} + +.quick-inputs { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 12rpx; + margin-bottom: 32rpx; +} + +.quick-btn { + padding: 16rpx; + background: #f8f9fa; + border-radius: 12rpx; + text-align: center; + font-size: 28rpx; + font-weight: 600; + color: #1A1A1A; + border: 2rpx solid rgba(0, 0, 0, 0.1); + transition: all 0.3s; +} + +.quick-btn:active { + background: rgba(0, 0, 0, 0.1); + transform: scale(0.95); +} + +.modal-actions { + padding: 0rpx; +} + +.btn-danger { + width: 100%; + background: linear-gradient(135deg, #ff3b30 0%, #e02020 100%); + color: white; + border-radius: 16rpx; + padding: 28rpx; + font-size: 32rpx; + font-weight: bold; + border: none; + box-shadow: 0 8rpx 24rpx rgba(255, 59, 48, 0.4); +} + +.btn-danger:active { + opacity: 0.9; + transform: scale(0.98); +} + +/* ========== 支出弹窗 ========== */ +.expense-list { + display: flex; + flex-direction: column; + gap: 12rpx; + margin-bottom: 32rpx; + max-height: 500rpx; + overflow-y: auto; +} + +.expense-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16rpx; + background: #f8f9fa; + border-radius: 12rpx; +} + +.expense-player { + display: flex; + align-items: center; + gap: 12rpx; + flex: 1; +} + +.expense-avatar { + width: 60rpx; + height: 60rpx; + border-radius: 50%; + border: 2rpx solid rgba(0, 0, 0, 0.1); +} + +.expense-nickname { + font-size: 28rpx; + color: #FFFFFF; + font-weight: 500; +} + +.expense-input { + width: 160rpx; + background: #1A1A1A; + border: 2rpx solid rgba(0, 0, 0, 0.1); + border-radius: 12rpx; + padding: 16rpx; + font-size: 28rpx; + color: #FFFFFF; + text-align: center; + font-weight: bold; +} + +.expense-input:focus { + border-color: #1A1A1A; + box-shadow: 0 0 0 4rpx rgba(0, 0, 0, 0.1); +} + +/* ========== 设置弹窗 ========== */ +.setting-section { + margin-bottom: 32rpx; +} + +.setting-section:last-child { + margin-bottom: 0; +} + +.setting-label { + display: block; + font-size: 28rpx; + font-weight: 600; + color: #FFFFFF; + margin-bottom: 16rpx; +} + +.setting-input { + width: 100%; + background: #f8f9fa; + border: 2rpx solid rgba(0, 0, 0, 0.1); + border-radius: 12rpx; + height: 60rpx; + padding-left: 24rpx; + font-size: 32rpx; + color: #FFFFFF; + text-align: center; + font-weight: bold; + box-sizing: border-box; +} + +.setting-input:focus { + border-color: #1A1A1A; + background: #1A1A1A; + box-shadow: 0 0 0 4rpx rgba(0, 0, 0, 0.1); +} + +.quick-input-settings { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12rpx; +} + +.quick-value-input { + background: #f8f9fa; + border: 2rpx solid rgba(0, 0, 0, 0.1); + border-radius: 12rpx; + padding: 20rpx; + font-size: 28rpx; + color: #FFFFFF; + text-align: center; + font-weight: bold; +} + +.quick-value-input:focus { + border-color: #1A1A1A; + background: #1A1A1A; + box-shadow: 0 0 0 4rpx rgba(0, 0, 0, 0.1); +} + +/* 语音播报开关 */ +.setting-row { + display: flex; + justify-content: space-between; + align-items: center; +} + +.setting-switch { + transform: scale(0.9); +} + +/* 快捷输入设置标题 */ +.label-row-settings { + display: flex; + align-items: center; + margin-bottom: 16rpx; +} + +.label-hint-settings { + font-size: 22rpx; + color: #B0B0B0; + margin-left: 8rpx; + font-weight: normal; +} + +/* 快捷输入网格布局(参考创建房间) */ +.quick-settings-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 12rpx; +} + +.quick-setting-item { + position: relative; + height: 80rpx; +} + +.quick-input-box { + width: 100%; + height: 100%; + background: #f8f9fa; + border: 2rpx solid rgba(0, 0, 0, 0.1); + border-radius: 12rpx; + padding: 0 10rpx; + font-size: 28rpx; + color: #FFFFFF; + box-sizing: border-box; + text-align: center; +} + +.quick-input-box:focus { + border-color: #1A1A1A; + background: #1A1A1A; + box-shadow: 0 0 0 4rpx rgba(0, 0, 0, 0.1); +} + +.remove-btn-settings { + position: absolute; + top: -8rpx; + right: -8rpx; + width: 40rpx; + height: 40rpx; + background: #ff3b30; + color: white; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 28rpx; + line-height: 1; + box-shadow: 0 2rpx 8rpx rgba(255, 59, 48, 0.3); + z-index: 1; +} + +.remove-btn-settings:active { + opacity: 0.8; + transform: scale(0.95); +} + +.add-btn-settings { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + gap: 8rpx; + height: 80rpx; + background: rgba(0, 0, 0, 0.05); + border: 2rpx dashed rgba(0, 0, 0, 0.3); + border-radius: 12rpx; + color: #1A1A1A; + font-size: 24rpx; +} + +.add-btn-settings:active { + background: rgba(0, 0, 0, 0.1); +} + +.add-icon-settings { + font-size: 36rpx; + font-weight: bold; + line-height: 1; +} + +.add-text-settings { + font-weight: 500; + font-size: 24rpx; +} + +/* ========== 分享弹窗 ========== */ +/* 二维码区域 */ +.qrcode-section { + text-align: center; + margin-bottom: 24rpx; +} + +.qrcode-image { + width: 400rpx; + height: 400rpx; + margin: 0 auto; + display: block; + border-radius: 12rpx; + background: #1A1A1A; +} + +.qrcode-loading { + width: 400rpx; + height: 400rpx; + margin: 0 auto; + background: #f5f5f5; + border-radius: 12rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 28rpx; + color: #B0B0B0; +} + +.invite-code-box { + background: #f8f9fa; + border-radius: 16rpx; + padding: 32rpx; + text-align: center; + margin-bottom: 24rpx; +} + +.code-label { + display: block; + font-size: 26rpx; + color: #B0B0B0; + margin-bottom: 12rpx; +} + +.code-value { + display: block; + font-size: 48rpx; + font-weight: bold; + color: #1A1A1A; + font-family: 'Courier New', monospace; + letter-spacing: 8rpx; + margin-bottom: 20rpx; +} + +.copy-btn { + background: linear-gradient(135deg, #1A1A1A 0%, #252525 100%); + color: white; + border-radius: 12rpx; + padding: 16rpx 48rpx; + font-size: 28rpx; + font-weight: bold; + border: none; +} + +.share-tip { + text-align: center; + padding: 24rpx 0; +} + +.share-tip text { + font-size: 26rpx; + color: #B0B0B0; +} diff --git a/pages/game/join/join.js b/pages/game/join/join.js new file mode 100644 index 0000000..b23d383 --- /dev/null +++ b/pages/game/join/join.js @@ -0,0 +1,144 @@ +// pages/game/join/join.js +import { onLoginReady } from '@jdmini/api' +import request from '../../../utils/request' + +Page({ + data: { + inviteCode: '', + joining: false, + recentSessions: [], + navbarHeight: 0 + }, + + onLoad(options) { + // 计算导航栏高度 + const windowInfo = wx.getWindowInfo() + const menuButtonInfo = wx.getMenuButtonBoundingClientRect() + const statusBarHeight = windowInfo.statusBarHeight + const menuButtonTop = menuButtonInfo.top + const navbarHeight = menuButtonInfo.bottom + (menuButtonTop - statusBarHeight) + this.setData({ navbarHeight }) + + + + // 检查是否从分享链接进入 + + if (options.code) { + console.log(options) + this.setData({ + inviteCode: options.code + }) + // 自动加入 + + } + + // 检查登录状态 + onLoginReady(()=>{ + //判断是否登录 + if (!getApp().getUserInfo()) { + wx.navigateTo({ + url: '/pages/login/login' + }) + return + } + + + + + if (options) { + this.joinSession() + } + //this.loadRecentSessions() + }) + // const app = getApp() + // if (!app.getUserInfo()) { + // wx.showToast({ + // title: '请先登录', + // icon: 'none' + // }) + // setTimeout(() => { + // wx.navigateBack() + // }, 1500) + // return + // } + // if (options) { + // this.joinSession() + // } + // // 加载最近的牌局 + // this.loadRecentSessions() + }, + + // 加载最近的牌局 + async loadRecentSessions() { + try { + const data = await request.get('/rooms/my-sessions', { + page: 1, + pageSize: 5 + }) + + this.setData({ + recentSessions: data.list || [] + }) + } catch (error) { + console.error('加载牌局失败:', error) + } + }, + + // 加入牌局 + async joinSession() { + const { inviteCode } = this.data + + if (!inviteCode) { + wx.showToast({ + title: '请输入邀请码', + icon: 'none' + }) + return + } + + if (this.data.joining) return + + this.setData({ joining: true }) + wx.showLoading({ title: '加入中...' }) + + try { + const data = await request.post('/rooms/join', { + room_code: inviteCode + }) + + wx.hideLoading() + wx.showToast({ + title: '加入成功', + icon: 'success' + }) + + // 更新全局牌局 + const app = getApp() + app.setCurrentSession(data.session) + + // 跳转到牌局详情 + setTimeout(() => { + wx.redirectTo({ + url: `/pages/game/detail/detail?id=${data.session.id}` + }) + }, 1500) + + } catch (error) { + wx.hideLoading() + wx.showToast({ + title: error.message || '加入失败', + icon: 'none' + }) + } finally { + this.setData({ joining: false }) + } + }, + + // 进入牌局 + goToSession(e) { + const id = e.currentTarget.dataset.id + wx.navigateTo({ + url: `/pages/game/detail/detail?id=${id}` + }) + } +}) \ No newline at end of file diff --git a/pages/game/join/join.json b/pages/game/join/join.json new file mode 100644 index 0000000..a9d8a74 --- /dev/null +++ b/pages/game/join/join.json @@ -0,0 +1 @@ +{"navigationStyle":"custom","usingComponents":{"custom-navbar":"../../../components/navbar/navbar"}} diff --git a/pages/game/join/join.wxml b/pages/game/join/join.wxml new file mode 100644 index 0000000..c33c7d7 --- /dev/null +++ b/pages/game/join/join.wxml @@ -0,0 +1,27 @@ + + + + + 加入牌局 + 输入邀请码即可加入好友的牌局 + + + + + + + + + * 邀请码由牌局创建者提供 + * 邀请码区分大小写 + + + + + \ No newline at end of file diff --git a/pages/game/join/join.wxss b/pages/game/join/join.wxss new file mode 100644 index 0000000..ea5a882 --- /dev/null +++ b/pages/game/join/join.wxss @@ -0,0 +1,173 @@ +/* 加入牌局页面样式 */ +page { + background: linear-gradient(135deg, #1A1A1A 0%, #252525 100%); + min-height: 100vh; +} + +.container { + padding-bottom: 120rpx; + padding-top: 120rpx; +} + +/* 加入卡片 */ +.join-card { + background: #1A1A1Afff; + border-radius: 20rpx; + padding: 48rpx 32rpx; + margin: 0 20rpx 40rpx; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.15); + text-align: center; +} + +.title { + font-size: 44rpx; + font-weight: bold; + color: #FFFFFF; + margin-bottom: 12rpx; +} + +.subtitle { + font-size: 26rpx; + color: #B0B0B0; + margin-bottom: 48rpx; +} + +/* 输入区域 */ +.input-section { + margin-bottom: 32rpx; +} + +.code-input { + width: 100%; + height: 80rpx; + padding-left: 28rpx; + background: #f8f9fa; + border: 2rpx solid rgba(0, 0, 0, 0.1); + border-radius: 12rpx; + font-size: 40rpx; + font-weight: bold; + text-align: center; + letter-spacing: 8rpx; + color: #FFFFFF; + font-family: 'Courier New', monospace; + box-sizing: border-box; + transition: all 0.3s; +} + +.code-input:focus { + border-color: #1A1A1A; + background: #1A1A1A; + box-shadow: 0 0 0 4rpx rgba(0, 0, 0, 0.1); +} + +/* 加入按钮 */ +.join-btn { + width: 100%; + padding: 28rpx; + background: linear-gradient(135deg, #50C878 0%, #06a854 100%); + color: white; + border-radius: 16rpx; + font-size: 32rpx; + font-weight: bold; + border: none; + box-shadow: 0 8rpx 24rpx rgba(7, 193, 96, 0.4); + margin-bottom: 24rpx; + transition: all 0.3s; +} + +.join-btn:active { + opacity: 0.9; + transform: scale(0.98); +} + +.join-btn[disabled] { + background: #e0e0e0; + color: #B0B0B0; + box-shadow: none; +} + +/* 提示信息 */ +.tips { + display: flex; + flex-direction: column; + gap: 8rpx; +} + +.tips text { + font-size: 24rpx; + color: #B0B0B0; + text-align: left; +} + +/* 最近牌局 */ +.recent-section { + margin: 0 20rpx; +} + +.section-title { + font-size: 32rpx; + font-weight: bold; + color: white; + margin-bottom: 20rpx; +} + +/* 牌局列表 */ +.session-list { + display: flex; + flex-direction: column; + gap: 16rpx; +} + +.session-item { + background: #1A1A1Afff; + border-radius: 20rpx; + padding: 24rpx; + display: flex; + justify-content: space-between; + align-items: center; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.15); +} + +.session-item:active { + background: #f8f8f8; +} + +.session-info { + flex: 1; +} + +.session-info .name { + font-size: 32rpx; + font-weight: 500; + color: #FFFFFF; + display: block; + margin-bottom: 8rpx; +} + +.session-info .code { + font-size: 24rpx; + color: #B0B0B0; + font-family: 'Courier New', monospace; +} + +.session-status { + padding: 8rpx 20rpx; + border-radius: 8rpx; + font-size: 24rpx; + font-weight: 500; +} + +.session-status.playing { + background: rgba(7, 193, 96, 0.1); + color: #50C878; +} + +.session-status.waiting { + background: rgba(255, 149, 0, 0.1); + color: #ff9500; +} + +.session-status.finished { + background: rgba(0, 0, 0, 0.1); + color: #1A1A1A; +} \ No newline at end of file diff --git a/pages/game/play/play.js b/pages/game/play/play.js new file mode 100644 index 0000000..ba2489b --- /dev/null +++ b/pages/game/play/play.js @@ -0,0 +1,566 @@ +// pages/game/play/play.js +import request from '../../../utils/request' + +Page({ + data: { + sessionId: null, + session: null, + players: [], + detailPlayers: [], + scoreboard: [], + recentRecords: [], + historyRecords: [], + currentRound: 1, + + // 快速记账 + selectedScore: 0, + customScore: '', + canSubmitQuick: false, + + // 详细记账 + showDetailMode: false, + totalBalance: 0, + + // 历史记录 + showHistory: false, + + // 刷新定时器 + refreshTimer: null + }, + + onLoad(options) { + if (!options.id) { + wx.showToast({ + title: '参数错误', + icon: 'none' + }) + setTimeout(() => { + wx.navigateBack() + }, 1500) + return + } + + this.setData({ sessionId: options.id }) + + // 加载数据 + this.loadSessionData() + this.loadRecentRecords() + + // 设置自动刷新 + this.startAutoRefresh() + }, + + onUnload() { + if (this.data.refreshTimer) { + clearInterval(this.data.refreshTimer) + } + }, + + // 启动自动刷新 + startAutoRefresh() { + const timer = setInterval(() => { + this.loadSessionData(true) + this.loadRecentRecords(true) + }, 10000) // 每10秒刷新一次 + + this.setData({ refreshTimer: timer }) + }, + + // 加载牌局数据 + async loadSessionData(silent = false) { + if (!silent) { + wx.showLoading({ title: '加载中...' }) + } + + try { + // 获取牌局详情 + const sessionData = await request.get(`/rooms/${this.data.sessionId}`) + + // 获取当前积分和局数 + const chips = await request.get(`/records/session/${this.data.sessionId}/chips`) + + // 准备玩家数据(用于快速记账) + const players = sessionData.players.map(p => ({ + id: p.player_id, + nickname: p.nickname, + avatar_url: p.avatar_url, + selected: false, // 是否选为赢家 + losing: false // 是否选为输家 + })) + + // 准备详细记账玩家数据 + const detailPlayers = sessionData.players.map(p => ({ + id: p.player_id, + nickname: p.nickname, + avatar_url: p.avatar_url, + current_chips: chips.chips[p.player_id] || 0, + chips_change: 0 + })) + + // 准备积分榜数据 + const scoreboard = sessionData.players.map(p => ({ + id: p.player_id, + nickname: p.nickname, + avatar_url: p.avatar_url, + total_chips: chips.chips[p.player_id] || 0 + })).sort((a, b) => b.total_chips - a.total_chips) + + this.setData({ + session: sessionData.session, + players, + detailPlayers, + scoreboard, + currentRound: chips.total_rounds + 1 + }) + + if (!silent) { + wx.hideLoading() + } + + } catch (error) { + if (!silent) { + wx.hideLoading() + wx.showToast({ + title: error.message || '加载失败', + icon: 'none' + }) + } + } + }, + + // 加载最近记录 + async loadRecentRecords(silent = false) { + try { + const recordsData = await request.get(`/records/session/${this.data.sessionId}`, { + page: 1, + pageSize: 5 + }) + + // 格式化记录 + const recentRecords = (recordsData.list || []).map(record => { + const time = new Date(record.created_at * 1000) + const now = Date.now() + const diff = now - record.created_at * 1000 + + return { + id: record.id, + round_number: record.round_number, + scores: record.playerScores || [], + timeText: this.formatRecordTime(time), + can_undo: diff < 300000 // 5分钟内可撤销 + } + }) + + this.setData({ recentRecords }) + + } catch (error) { + if (!silent) { + console.error('加载记录失败:', error) + } + } + }, + + // 格式化时间 + formatRecordTime(time) { + const now = new Date() + const diff = now - time + + if (diff < 60000) { + return '刚刚' + } else if (diff < 3600000) { + return `${Math.floor(diff / 60000)}分钟前` + } else { + return `${time.getHours()}:${String(time.getMinutes()).padStart(2, '0')}` + } + }, + + // 切换赢家 + toggleWinner(e) { + const playerId = e.currentTarget.dataset.id + const players = this.data.players.map(p => { + if (p.id === playerId) { + p.selected = !p.selected + // 赢家不能同时是输家 + if (p.selected) p.losing = false + } + return p + }) + + this.setData({ players }) + this.checkCanSubmitQuick() + }, + + // 切换输家 + toggleLoser(e) { + const playerId = e.currentTarget.dataset.id + const players = this.data.players.map(p => { + if (p.id === playerId) { + p.losing = !p.losing + // 输家不能同时是赢家 + if (p.losing) p.selected = false + } + return p + }) + + this.setData({ players }) + this.checkCanSubmitQuick() + }, + + // 设置分数 + setScore(e) { + const score = parseInt(e.currentTarget.dataset.score) + this.setData({ + selectedScore: score, + customScore: '' + }) + this.checkCanSubmitQuick() + }, + + // 输入自定义分数 + onScoreInput(e) { + const score = parseInt(e.detail.value) || 0 + this.setData({ + customScore: e.detail.value, + selectedScore: score + }) + this.checkCanSubmitQuick() + }, + + // 检查是否可以提交快速记账 + checkCanSubmitQuick() { + const { players, selectedScore } = this.data + const winners = players.filter(p => p.selected) + const losers = players.filter(p => p.losing) + + const canSubmit = winners.length > 0 && losers.length > 0 && selectedScore > 0 + this.setData({ canSubmitQuick: canSubmit }) + }, + + // 提交快速记账 + async submitQuickEntry() { + const { players, selectedScore, currentRound } = this.data + const winners = players.filter(p => p.selected) + const losers = players.filter(p => p.losing) + + if (winners.length === 0 || losers.length === 0 || selectedScore <= 0) { + wx.showToast({ + title: '请完整填写信息', + icon: 'none' + }) + return + } + + // 计算分数变化 + const winnerShare = Math.floor(selectedScore * losers.length / winners.length) + const loserShare = selectedScore + + const scores = {} + winners.forEach(w => { + scores[w.id] = winnerShare + }) + losers.forEach(l => { + scores[l.id] = -loserShare + }) + + // 未参与的玩家分数为0 + players.forEach(p => { + if (!scores[p.id]) { + scores[p.id] = 0 + } + }) + + wx.showLoading({ title: '提交中...' }) + + try { + await request.post('/records/create', { + session_id: this.data.sessionId, + round_number: currentRound, + scores + }) + + wx.hideLoading() + wx.showToast({ + title: '记账成功', + icon: 'success' + }) + + // 重置选择 + const resetPlayers = players.map(p => ({ + ...p, + selected: false, + losing: false + })) + + this.setData({ + players: resetPlayers, + selectedScore: 0, + customScore: '', + canSubmitQuick: false, + currentRound: currentRound + 1 + }) + + // 刷新数据 + this.loadSessionData() + this.loadRecentRecords() + + } catch (error) { + wx.hideLoading() + wx.showToast({ + title: error.message || '提交失败', + icon: 'none' + }) + } + }, + + // 切换详细记账模式 + toggleDetailMode() { + this.setData({ + showDetailMode: !this.data.showDetailMode + }) + }, + + // 详细分数输入 + onDetailScoreInput(e) { + const playerId = e.currentTarget.dataset.id + const value = parseInt(e.detail.value) || 0 + + const detailPlayers = this.data.detailPlayers.map(p => { + if (p.id === playerId) { + p.chips_change = value + } + return p + }) + + // 计算总平衡 + const totalBalance = detailPlayers.reduce((sum, p) => sum + p.chips_change, 0) + + this.setData({ + detailPlayers, + totalBalance + }) + }, + + // 提交详细记账 + async submitDetailEntry() { + const { detailPlayers, totalBalance, currentRound } = this.data + + if (totalBalance !== 0) { + wx.showToast({ + title: '分数需要平衡', + icon: 'none' + }) + return + } + + // 检查是否有变化 + const hasChange = detailPlayers.some(p => p.chips_change !== 0) + if (!hasChange) { + wx.showToast({ + title: '请输入分数变化', + icon: 'none' + }) + return + } + + // 构建分数对象 + const scores = {} + detailPlayers.forEach(p => { + scores[p.id] = p.chips_change + }) + + wx.showLoading({ title: '提交中...' }) + + try { + await request.post('/records/create', { + session_id: this.data.sessionId, + round_number: currentRound, + scores + }) + + wx.hideLoading() + wx.showToast({ + title: '记账成功', + icon: 'success' + }) + + // 重置详细记账 + const resetPlayers = detailPlayers.map(p => ({ + ...p, + chips_change: 0 + })) + + this.setData({ + detailPlayers: resetPlayers, + totalBalance: 0, + showDetailMode: false, + currentRound: currentRound + 1 + }) + + // 刷新数据 + this.loadSessionData() + this.loadRecentRecords() + + } catch (error) { + wx.hideLoading() + wx.showToast({ + title: error.message || '提交失败', + icon: 'none' + }) + } + }, + + // 撤销记录 + async undoRecord(e) { + const recordId = e.currentTarget.dataset.id + + wx.showModal({ + title: '撤销记录', + content: '确定要撤销这条记录吗?', + success: async (res) => { + if (res.confirm) { + wx.showLoading({ title: '撤销中...' }) + + try { + await request.delete(`/records/${recordId}`) + + wx.hideLoading() + wx.showToast({ + title: '已撤销', + icon: 'success' + }) + + // 刷新数据 + this.loadSessionData() + this.loadRecentRecords() + + } catch (error) { + wx.hideLoading() + wx.showToast({ + title: error.message || '撤销失败', + icon: 'none' + }) + } + } + } + }) + }, + + // 查看历史记录 + async viewHistory() { + wx.showLoading({ title: '加载中...' }) + + try { + const recordsData = await request.get(`/records/session/${this.data.sessionId}`, { + page: 1, + pageSize: 50 + }) + + const historyRecords = (recordsData.list || []).map(record => { + const time = new Date(record.created_at * 1000) + return { + ...record, + timeText: `${time.getMonth() + 1}/${time.getDate()} ${time.getHours()}:${String(time.getMinutes()).padStart(2, '0')}` + } + }) + + this.setData({ + historyRecords, + showHistory: true + }) + + wx.hideLoading() + + } catch (error) { + wx.hideLoading() + wx.showToast({ + title: '加载失败', + icon: 'none' + }) + } + }, + + // 关闭历史记录 + closeHistory() { + this.setData({ + showHistory: false + }) + }, + + // 暂停游戏 + pauseGame() { + wx.showModal({ + title: '暂停游戏', + content: '确定要暂停游戏吗?', + success: async (res) => { + if (res.confirm) { + try { + await request.post(`/rooms/${this.data.sessionId}/pause`) + + wx.showToast({ + title: '已暂停', + icon: 'success' + }) + + setTimeout(() => { + wx.navigateBack() + }, 1500) + + } catch (error) { + wx.showToast({ + title: error.message || '操作失败', + icon: 'none' + }) + } + } + } + }) + }, + + // 结束游戏 + endGame() { + wx.showModal({ + title: '结束牌局', + content: '确定要结束牌局吗?结束后将无法继续记账', + confirmColor: '#ff4444', + success: async (res) => { + if (res.confirm) { + wx.showLoading({ title: '处理中...' }) + + try { + await request.post(`/rooms/${this.data.sessionId}/end`) + + wx.hideLoading() + wx.showToast({ + title: '牌局已结束', + icon: 'success' + }) + + // 跳转到统计页 + setTimeout(() => { + wx.redirectTo({ + url: `/pages/stats/session/session?id=${this.data.sessionId}` + }) + }, 1500) + + } catch (error) { + wx.hideLoading() + wx.showToast({ + title: error.message || '操作失败', + icon: 'none' + }) + } + } + } + }) + }, + + // 下拉刷新 + onPullDownRefresh() { + Promise.all([ + this.loadSessionData(), + this.loadRecentRecords() + ]).then(() => { + wx.stopPullDownRefresh() + }) + } +}) \ No newline at end of file diff --git a/pages/game/play/play.json b/pages/game/play/play.json new file mode 100644 index 0000000..c202186 --- /dev/null +++ b/pages/game/play/play.json @@ -0,0 +1,6 @@ +{ + "navigationStyle": "custom", + "usingComponents": { + "custom-navbar": "../../../components/navbar/navbar" + } +} \ No newline at end of file diff --git a/pages/game/play/play.wxml b/pages/game/play/play.wxml new file mode 100644 index 0000000..0aafc7b --- /dev/null +++ b/pages/game/play/play.wxml @@ -0,0 +1,183 @@ + + + + + + + {{session.session_name}} + 第 {{currentRound}} 局 + + + + + + + + + 快速记账 + + + + + + + + {{item.nickname}} + + + + + + + + + + + + {{item.nickname}} + + + + + + + + + + + + + + + + + + + + + + + + + 详细记账 + + {{showDetailMode ? '收起' : '展开'}} + + + + + + + + + {{item.nickname}} + + + + 当前: {{item.current_chips}} + + + + + + + 总计: + + {{totalBalance > 0 ? '+' : ''}}{{totalBalance}} + + + 需要平衡到0 + + + + + + + + + + 当前积分榜 + + + + {{index + 1}} + + + {{item.nickname}} + + + {{item.total_chips > 0 ? '+' : ''}}{{item.total_chips}} + + + + + + + + + 最近5局 + + + + + 第{{item.round_number}}局 + + + {{score.nickname}}: {{score.chips_change > 0 ? '+' : ''}}{{score.chips_change}} + + + {{item.timeText}} + 撤销 + + + + + + + + + + + + + + + + + 历史记录 + × + + + + + + 第{{item.round_number}}局 + {{item.timeText}} + + + + {{score.nickname}} + + {{score.chips_change > 0 ? '+' : ''}}{{score.chips_change}} + + + + + + + \ No newline at end of file diff --git a/pages/game/play/play.wxss b/pages/game/play/play.wxss new file mode 100644 index 0000000..de663f3 --- /dev/null +++ b/pages/game/play/play.wxss @@ -0,0 +1,412 @@ +/* 实时记账页面样式 */ +page { + background: linear-gradient(135deg, #1A1A1A 0%, #252525 100%); + min-height: 100vh; +} + +.container { + padding: 30rpx; + padding-bottom: 120rpx; +} + +/* 顶部信息 */ +.session-header { + display: flex; + justify-content: space-between; + align-items: center; + background: linear-gradient(135deg, #1A1A1A 0%, #252525 100%); + color: white; +} + +.header-info { + flex: 1; +} + +.session-name { + font-size: 32rpx; + font-weight: bold; + margin-bottom: 8rpx; + display: block; +} + +.round-number { + font-size: 26rpx; + opacity: 0.9; +} + +.header-actions button { + background: rgba(255, 255, 255, 0.2); + color: white; + font-size: 26rpx; + padding: 12rpx 24rpx; +} + +/* 快速记账区 */ +.quick-entry { + background: #1A1A1A; + margin-bottom: 20rpx; +} + +.section-label { + font-size: 28rpx; + color: #E0E0E0; + margin-bottom: 16rpx; + display: block; +} + +.player-select { + display: flex; + flex-wrap: wrap; + gap: 16rpx; + margin-bottom: 24rpx; +} + +.player-option { + display: flex; + align-items: center; + padding: 12rpx 20rpx; + border: 2rpx solid #e5e5e5; + border-radius: 12rpx; + background: #f8f8f8; + position: relative; + transition: all 0.2s ease; +} + +.player-option.selected { + background: #e8f5e9; + border-color: #50C878; +} + +.player-option.losing { + background: #ffebee; + border-color: #ff3b30; +} + +.player-name { + font-size: 28rpx; + color: #FFFFFF; + margin-left: 8rpx; +} + +.check-mark { + position: absolute; + right: 8rpx; + top: 50%; + transform: translateY(-50%); + width: 24rpx; + height: 24rpx; + border-radius: 50%; + background: #50C878; + color: white; + font-size: 16rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.check-mark.lose { + background: #ff3b30; +} + +/* 分数输入区 */ +.score-section { + margin-top: 24rpx; +} + +.score-input-group { + display: flex; + gap: 12rpx; + align-items: center; +} + +.score-preset { + flex: 1; + background: #f0f0f0; + border: none; + padding: 16rpx; + border-radius: 8rpx; + font-size: 30rpx; + color: #FFFFFF; + text-align: center; +} + +.score-preset:active { + background: #50C878; + color: white; +} + +.score-input { + flex: 2; + background: #f8f8f8; + border: 1rpx solid #e5e5e5; + padding: 16rpx; + border-radius: 8rpx; + font-size: 30rpx; + text-align: center; +} + +.submit-btn { + width: 100%; + margin-top: 24rpx; +} + +/* 详细记账区 */ +.detailed-entry { + margin-bottom: 20rpx; +} + +.toggle-btn { + color: #50C878; + font-size: 26rpx; +} + +.detail-content { + margin-top: 20rpx; +} + +.player-entry { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16rpx 0; + border-bottom: 1rpx solid #f0f0f0; +} + +.player-entry:last-child { + border-bottom: none; +} + +.player-info { + display: flex; + align-items: center; + flex: 1; +} + +.score-entry { + display: flex; + align-items: center; + gap: 16rpx; +} + +.current-chips { + font-size: 26rpx; + color: #B0B0B0; +} + +.chips-input { + width: 120rpx; + text-align: center; + font-weight: bold; +} + +.chips-input.win { + color: #50C878; + border-color: #50C878; +} + +.chips-input.lose { + color: #ff3b30; + border-color: #ff3b30; +} + +/* 平衡检查 */ +.balance-check { + display: flex; + align-items: center; + justify-content: center; + padding: 20rpx; + background: #f8f8f8; + border-radius: 8rpx; + margin: 20rpx 0; +} + +.balance-label { + font-size: 28rpx; + color: #E0E0E0; + margin-right: 16rpx; +} + +.balance-value { + font-size: 36rpx; + font-weight: bold; +} + +.balance-value.balanced { + color: #50C878; +} + +.balance-value.unbalanced { + color: #ff3b30; +} + +.balance-tip { + font-size: 24rpx; + color: #ff3b30; + margin-left: 16rpx; +} + +/* 积分榜 */ +.scoreboard { + margin-bottom: 20rpx; +} + +.scoreboard-list { + margin-top: 20rpx; +} + +.scoreboard-item { + display: flex; + align-items: center; + padding: 16rpx 0; + border-bottom: 1rpx solid #f0f0f0; +} + +.scoreboard-item:last-child { + border-bottom: none; +} + +.rank { + width: 48rpx; + height: 48rpx; + border-radius: 50%; + background: #f0f0f0; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + font-size: 28rpx; + margin-right: 20rpx; +} + +.scoreboard-item:nth-child(1) .rank { + background: linear-gradient(135deg, #ffd700 0%, #ffed4e 100%); + color: white; +} + +.scoreboard-item:nth-child(2) .rank { + background: linear-gradient(135deg, #c0c0c0 0%, #e0e0e0 100%); + color: white; +} + +.scoreboard-item:nth-child(3) .rank { + background: linear-gradient(135deg, #cd7f32 0%, #daa520 100%); + color: white; +} + +.player-score { + font-size: 32rpx; + font-weight: bold; + margin-left: auto; +} + +/* 最近记录 */ +.recent-records { + margin-bottom: 100rpx; +} + +.record-list { + margin-top: 20rpx; +} + +.record-item { + padding: 16rpx; + background: #f8f8f8; + border-radius: 8rpx; + margin-bottom: 12rpx; + position: relative; +} + +.record-round { + font-size: 26rpx; + font-weight: bold; + color: #E0E0E0; + margin-bottom: 8rpx; +} + +.record-scores { + display: flex; + flex-wrap: wrap; + gap: 12rpx; + margin-bottom: 8rpx; +} + +.score-item { + font-size: 24rpx; + padding: 4rpx 12rpx; + background: #1A1A1A; + border-radius: 4rpx; +} + +.record-time { + font-size: 22rpx; + color: #B0B0B0; +} + +.undo-btn { + position: absolute; + right: 16rpx; + top: 50%; + transform: translateY(-50%); + color: #ff3b30; + font-size: 24rpx; + padding: 8rpx 16rpx; + background: #1A1A1A; + border-radius: 4rpx; +} + +/* 历史记录弹窗 */ +.history-modal .modal-content { + width: 90%; + max-width: none; +} + +.history-item { + padding: 20rpx; + border-bottom: 1rpx solid #f0f0f0; +} + +.history-item:last-child { + border-bottom: none; +} + +.history-header { + display: flex; + justify-content: space-between; + margin-bottom: 12rpx; +} + +.history-round { + font-size: 28rpx; + font-weight: bold; + color: #FFFFFF; +} + +.history-time { + font-size: 24rpx; + color: #B0B0B0; +} + +.history-scores { + display: flex; + flex-wrap: wrap; + gap: 12rpx; +} + +.history-score { + display: flex; + align-items: center; + padding: 8rpx 16rpx; + background: #f8f8f8; + border-radius: 6rpx; +} + +.history-score .player-name { + font-size: 24rpx; + color: #E0E0E0; + margin-right: 8rpx; +} + +.history-score .score-change { + font-size: 26rpx; + font-weight: bold; +} \ No newline at end of file diff --git a/pages/game/settlement/settlement.js b/pages/game/settlement/settlement.js new file mode 100644 index 0000000..a4d776b --- /dev/null +++ b/pages/game/settlement/settlement.js @@ -0,0 +1,133 @@ +// pages/game/settlement/settlement.js +import request from '../../../utils/request' + +Page({ + data: { + roomId: null, + navbarHeight: 0, + isHost: false, + userInfo: null, + settlements: [], // 所有结算记录(房主) + mySettlements: [], // 我的结算记录(房客) + myTotal: 0 // 我的总盈亏 + }, + + onLoad(options) { + // 计算导航栏高度 + const windowInfo = wx.getWindowInfo() + const menuButtonInfo = wx.getMenuButtonBoundingClientRect() + const statusBarHeight = windowInfo.statusBarHeight + const menuButtonTop = menuButtonInfo.top + const navbarHeight = menuButtonInfo.bottom + (menuButtonTop - statusBarHeight) + this.setData({ navbarHeight }) + + if (!options.id) { + wx.showToast({ + title: '房间不存在', + icon: 'none' + }) + setTimeout(() => { + wx.navigateBack() + }, 1500) + return + } + + this.setData({ roomId: options.id }) + + // 获取用户信息 + const app = getApp() + const userInfo = app.getUserInfo() + if (!userInfo) { + wx.showToast({ + title: '请先登录', + icon: 'none' + }) + setTimeout(() => { + wx.navigateBack() + }, 1500) + return + } + + this.setData({ userInfo }) + + // 加载结算数据 + this.loadSettlementData() + }, + + // 加载结算数据 + async loadSettlementData() { + wx.showLoading({ title: '加载中...' }) + + try { + const data = await request.get(`/rooms/${this.data.roomId}/settlement`) + + this.setData({ + isHost: data.is_host, + settlements: data.settlements || [], + mySettlements: data.my_settlements || [], + myTotal: data.my_total || 0 + }) + + wx.hideLoading() + } catch (error) { + wx.hideLoading() + wx.showToast({ + title: error.message || '加载失败', + icon: 'none' + }) + } + }, + + // 确认结算 + confirmSettle() { + const title = this.data.isHost ? '确认关闭房间?' : '确认退出房间?' + const content = this.data.isHost + ? '房间将被关闭,结算记录已保存,所有玩家将被移除' + : '您将退出房间,稍后可以重新加入。您的结算记录已保存' + + wx.showModal({ + title: title, + content: content, + confirmText: '确认', + cancelText: '取消', + success: (res) => { + if (res.confirm) { + this.doSettle() + } + } + }) + }, + + // 执行结算 + async doSettle() { + wx.showLoading({ title: '结算中...' }) + + try { + const endpoint = this.data.isHost + ? `/rooms/${this.data.roomId}/close` + : `/rooms/${this.data.roomId}/leave` + + await request.post(endpoint) + + wx.hideLoading() + wx.showToast({ + title: this.data.isHost ? '房间已关闭' : '已退出房间', + icon: 'success' + }) + + setTimeout(() => { + // 返回首页 + wx.reLaunch({ + url: '/pages/index/index' + }) + }, 1500) + + } catch (error) { + wx.hideLoading() + wx.showToast({ + title: error.message || '结算失败', + icon: 'none' + }) + } + } +}) diff --git a/pages/game/settlement/settlement.json b/pages/game/settlement/settlement.json new file mode 100644 index 0000000..2854c29 --- /dev/null +++ b/pages/game/settlement/settlement.json @@ -0,0 +1,6 @@ +{ + "navigationStyle": "custom", + "usingComponents": { + "custom-navbar": "../../../components/navbar/navbar" + } +} diff --git a/pages/game/settlement/settlement.wxml b/pages/game/settlement/settlement.wxml new file mode 100644 index 0000000..fb34843 --- /dev/null +++ b/pages/game/settlement/settlement.wxml @@ -0,0 +1,103 @@ + + +module.exports = { + abs: function(num) { + return num < 0 ? -num : num; + } +} + + + + + + + + {{isHost ? '👑' : '👤'}} + {{isHost ? '您是房主,结算将关闭房间' : '您是房客,结算仅结算自己'}} + + + + + 结算信息 + + + + + + + {{item.from_nickname}} + + + + {{item.amount > 0 ? '收取' : '支付给'}} + + + + + {{item.to_nickname}} + + + + {{item.amount > 0 ? '+' : ''}}{{math.abs(item.amount)}} + + + + + + 💰 + 暂无结算信息 + + + + + + + + + {{userInfo.nickname}} + + + + {{item.amount > 0 ? '收取' : '支付给'}} + + + + + {{item.other_nickname}} + + + + {{item.amount > 0 ? '+' : ''}}{{math.abs(item.amount)}} + + + + + + 💰 + 您已结清 + + + + + + + 汇总 + + 总交易笔数 + {{isHost ? settlements.length : mySettlements.length}} 笔 + + + 我的盈亏 + + {{myTotal > 0 ? '+' : ''}}{{myTotal}} + + + + + + + + + diff --git a/pages/game/settlement/settlement.wxss b/pages/game/settlement/settlement.wxss new file mode 100644 index 0000000..99ce7c7 --- /dev/null +++ b/pages/game/settlement/settlement.wxss @@ -0,0 +1,219 @@ +/* 结算页面样式 */ +page { + background: linear-gradient(135deg, #1A1A1A 0%, #252525 100%); + min-height: 100vh; +} + +.container { + padding: 20rpx 30rpx 140rpx; +} + +/* 身份提示 */ +.role-tip { + background: rgba(255, 255, 255, 0.95); + border-radius: 16rpx; + padding: 24rpx; + margin-bottom: 20rpx; + display: flex; + align-items: center; + gap: 16rpx; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.15); +} + +.role-icon { + font-size: 40rpx; +} + +.role-text { + flex: 1; + font-size: 28rpx; + color: #E0E0E0; + line-height: 1.5; +} + +/* 结算卡片 */ +.settlement-card, +.summary-card { + background: rgba(255, 255, 255, 0.95); + border-radius: 20rpx; + padding: 32rpx 24rpx; + margin-bottom: 20rpx; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.15); +} + +.card-title { + font-size: 32rpx; + font-weight: bold; + color: #FFFFFF; + margin-bottom: 24rpx; +} + +/* 结算列表 */ +.settlement-list { + display: flex; + flex-direction: column; + gap: 20rpx; +} + +.settlement-item { + display: grid; + grid-template-columns: 1fr auto 1fr auto; + gap: 16rpx; + align-items: center; + padding: 20rpx; + background: #f8f9fa; + border-radius: 16rpx; + transition: all 0.3s; +} + +.settlement-item:active { + background: #f0f0f0; + transform: scale(0.98); +} + +.player-from, +.player-to { + display: flex; + align-items: center; + gap: 12rpx; + min-width: 0; +} + +.avatar { + width: 60rpx; + height: 60rpx; + border-radius: 50%; + border: 2rpx solid rgba(0, 0, 0, 0.1); + flex-shrink: 0; +} + +.nickname { + font-size: 28rpx; + color: #FFFFFF; + font-weight: 500; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.settlement-action { + padding: 8rpx 16rpx; + background: rgba(0, 0, 0, 0.1); + border-radius: 8rpx; +} + +.action-text { + font-size: 24rpx; + color: #1A1A1A; + white-space: nowrap; +} + +.amount { + font-size: 32rpx; + font-weight: bold; + text-align: right; + white-space: nowrap; +} + +.amount.win { + color: #50C878; +} + +.amount.lose { + color: #fa5151; +} + +/* 空状态 */ +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 80rpx 0; +} + +.empty-icon { + font-size: 80rpx; + margin-bottom: 16rpx; + opacity: 0.5; +} + +.empty-text { + font-size: 28rpx; + color: #B0B0B0; +} + +/* 汇总卡片 */ +.summary-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16rpx 0; + border-bottom: 1rpx solid #f0f0f0; +} + +.summary-row:last-child { + border-bottom: none; +} + +.summary-label { + font-size: 28rpx; + color: #E0E0E0; +} + +.summary-value { + font-size: 32rpx; + font-weight: bold; + color: #FFFFFF; +} + +.summary-value.win { + color: #50C878; +} + +.summary-value.lose { + color: #fa5151; +} + +/* 底部按钮 */ +.bottom-bar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background: #1A1A1A; + padding: 16rpx 30rpx; + box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.1); + z-index: 100; + padding-bottom: env(safe-area-inset-bottom); +} + +.settle-btn { + width: 100%; + height: 88rpx; + border-radius: 16rpx; + font-size: 32rpx; + font-weight: bold; + border: none; + color: white; +} + +.btn-primary { + background: linear-gradient(135deg, #1A1A1A 0%, #252525 100%); + box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.4); +} + +.btn-primary:active { + opacity: 0.9; + transform: translateY(2rpx); +} + +.btn-danger { + background: linear-gradient(135deg, #ff3b30 0%, #e02020 100%); + box-shadow: 0 8rpx 24rpx rgba(255, 59, 48, 0.4); +} + +.btn-danger:active { + opacity: 0.9; + transform: translateY(2rpx); +} diff --git a/pages/index/index.js b/pages/index/index.js new file mode 100644 index 0000000..305f2d9 --- /dev/null +++ b/pages/index/index.js @@ -0,0 +1,291 @@ +import { injectPage ,onLoginReady } from '@jdmini/api' +import request from '../../utils/request' + +Page(injectPage({})({ + data: { + userInfo: null, + isLogin: false, + activeRooms: [], + recentRecords: [], + navbarHeight: 0, + showGuide: false, + guideSteps: [ + { + selector: '.guide-create-btn', + title: '创建房间', + description: '点击这里可以创建一个新的牌局房间,邀请好友一起玩', + position: 'bottom', + padding: 12, + borderRadius: 16 + }, + { + selector: '.guide-join-btn', + title: '加入房间', + description: '输入房间号或使用扫码功能,快速加入好友的牌局', + position: 'bottom', + padding: 12, + borderRadius: 16 + }, + { + selector: '.guide-scan-btn', + title: '扫码进入', + description: '扫描房间二维码,即可立即进入牌局,非常方便!', + position: 'bottom', + padding: 8, + borderRadius: 12 + } + ] + }, + + onLoad() { + console.log('首页加载') + + // 计算导航栏高度 + const windowInfo = wx.getWindowInfo() + const menuButtonInfo = wx.getMenuButtonBoundingClientRect() + const statusBarHeight = windowInfo.statusBarHeight + const menuButtonTop = menuButtonInfo.top + const navbarHeight = menuButtonInfo.bottom + (menuButtonTop - statusBarHeight) + this.setData({ navbarHeight }) + + // 加载用户信息(如果已登录) + // this.loadUserInfo() + }, + + onShow() { + // 每次显示页面时刷新用户信息和数据 + onLoginReady(()=>{ + this.loadUserInfo() + }) + }, + + onReady() { + // 页面渲染完成后,检查是否需要显示引导 + this.checkAndShowGuide() + }, + + // 检查并显示新用户引导 + checkAndShowGuide() { + try { + const hasShownGuide = wx.getStorageSync('hasShownGuide') + + if (!hasShownGuide) { + // 延迟显示引导,等待页面完全渲染 + setTimeout(() => { + this.setData({ showGuide: true }) + }, 500) + } + } catch (error) { + console.error('检查引导状态失败:', error) + } + }, + + // 引导完成回调 + onGuideComplete() { + this.setData({ showGuide: false }) + + // 标记已显示过引导 + try { + wx.setStorageSync('hasShownGuide', true) + } catch (error) { + console.error('保存引导状态失败:', error) + } + }, + + // 加载用户信息 + async loadUserInfo() { + const app = getApp() + const userInfo = app.getUserInfo() + console.log(userInfo) + if (userInfo) { + // 已登录,从后端获取最新用户信息 + try { + const data = await request.get('/auth/profile') + // 更新本地缓存 + app.setUserInfo(data) + // 显示最新数据 + this.setData({ + userInfo: data, + isLogin: true + }) + this.loadActiveRooms() + } catch (error) { + console.error('获取用户信息失败:', error) + // 如果获取失败,使用本地缓存数据 + this.setData({ + userInfo, + isLogin: true + }) + this.loadActiveRooms() + } + } else { + // 未登录,显示登录按钮 + this.setData({ + userInfo: null, + isLogin: false, + activeRooms: [] + }) + } + }, + + // 检查是否登录,未登录则跳转 + checkLoginAndNavigate() { + if (!this.data.isLogin) { + wx.navigateTo({ + url: '/pages/login/login' + }) + return false + } + return true + }, + + // 格式化时间 + formatTime(timestamp) { + const date = new Date(timestamp * 1000) + const now = new Date() + const diff = now - date + + // 小于1分钟 + if (diff < 60000) { + return '刚刚' + } + + // 小于1小时 + if (diff < 3600000) { + return `${Math.floor(diff / 60000)}分钟前` + } + + // 小于24小时 + if (diff < 86400000) { + return `${Math.floor(diff / 3600000)}小时前` + } + + // 小于7天 + if (diff < 604800000) { + return `${Math.floor(diff / 86400000)}天前` + } + + // 格式化为 月-日 时:分 + 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') + return `${month}-${day} ${hour}:${minute}` + }, + + // 加载进行中的房间 + async loadActiveRooms() { + if (!this.data.isLogin) return + + try { + const data = await request.get('/rooms/my-sessions', { + status: 'active', + pageSize: 5 + }) + + // 处理房间数据,添加格式化的时间 + const rooms = (data.list || []).map(room => ({ + ...room, + create_time: this.formatTime(room.created_at) + })) + + this.setData({ + activeRooms: rooms + }) + } catch (error) { + console.error('加载房间失败:', error) + } + }, + + // 进入个人资料页(登录/修改信息) + goToProfile() { + wx.navigateTo({ + url: '/pages/login/login' + }) + }, + + // 处理登录(保留兼容) + async handleLogin() { + this.goToProfile() + }, + + // 创建房间 + createRoom() { + if (!this.checkLoginAndNavigate()) { + return + } + + wx.navigateTo({ + url: '/pages/game/create/create' + }) + }, + + // 扫码进入房间 + scanCode() { + if (!this.checkLoginAndNavigate()) { + return + } + + wx.scanCode({ + onlyFromCamera: false, + scanType: ['wxCode','qrCode'], + success: (res) => { + console.log('扫码结果:', res) + // 解析二维码内容 + const result = res.path || res.result + // 检查是否是有效的路径(兼容带/和不带/的路径) + if (result.includes('pages/game/join/join?scene')) { + // 提取邀请码 + //const match = result.split("%3D") + // console.log(match) + const match = result.match(/\%3D([A-Z0-9]+)/) + if (match && match[1]) { + const inviteCode = match[1] + // 跳转到加入页面 + wx.navigateTo({ + url: `/pages/game/join/join?code=${inviteCode}` + }) + } else { + wx.showToast({ + title: '无效的二维码', + icon: 'none' + }) + } + } else { + wx.showToast({ + title: '不是有效的房间二维码', + icon: 'none' + }) + } + }, + fail: (err) => { + console.error('扫码失败:', err) + if (err.errMsg !== 'scanCode:fail cancel') { + wx.showToast({ + title: '扫码失败', + icon: 'none' + }) + } + } + }) + }, + + // 加入房间 + joinRoom() { + if (!this.checkLoginAndNavigate()) { + return + } + + wx.navigateTo({ + url: '/pages/game/join/join' + }) + }, + + // 进入房间详情 + goToRoom(e) { + const id = e.currentTarget.dataset.id + wx.navigateTo({ + url: `/pages/game/detail/detail?id=${id}` + }) + } +})) diff --git a/pages/index/index.json b/pages/index/index.json new file mode 100644 index 0000000..98503ec --- /dev/null +++ b/pages/index/index.json @@ -0,0 +1,9 @@ +{ + "navigationStyle": "custom", + "usingComponents": { + "jdwx-ad": "@jdmini/components/jdwx-ad", + "custom-tabbar": "../../components/custom-tabbar/custom-tabbar", + "custom-navbar": "../../components/navbar/navbar", + "user-guide": "../../components/guide/guide" + } +} \ No newline at end of file diff --git a/pages/index/index.wxml b/pages/index/index.wxml new file mode 100644 index 0000000..faf746c --- /dev/null +++ b/pages/index/index.wxml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + 📷 + 扫码进入 + + + + + + + 场次 + {{userInfo.total_games || 0}} + + + + 胜率 + {{userInfo.win_rate || 0}}% + + + + + {{userInfo.total_win || 0}} + + + + + {{userInfo.total_loss || 0}} + + + + + + + + + + 创建房间 + + + + 加入房间 + + + + + + + 进行中的房间 + + + + + + + {{item.room_name}} + 房主 + + + + + + {{item.status === 'playing' ? '进行中' : '等待中'}} + + {{item.current_players}}人 + + + + + {{item.create_time}} + + + + + + + + 🎯 + 暂无房间 + 快创建一个房间开始游戏吧 + + + + + + + + + + \ No newline at end of file diff --git a/pages/index/index.wxss b/pages/index/index.wxss new file mode 100644 index 0000000..9c4237a --- /dev/null +++ b/pages/index/index.wxss @@ -0,0 +1,346 @@ +/* 首页样式 - 黑金主题 */ +page { + background: linear-gradient(180deg, #000000 0%, #0A0A0A 100%); + min-height: 100vh; +} + +/* 容器 */ +.container { + padding: 30rpx; + padding-bottom: 120rpx; +} + +/* 用户信息卡片 */ +.user-card { + background: linear-gradient(135deg, #1A1A1A 0%, #252525 100%); + border-radius: 20rpx; + padding: 32rpx 24rpx; + margin-bottom: 30rpx; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.8); + border: 1rpx solid rgba(212, 175, 55, 0.3); + position: relative; + overflow: hidden; +} + +/* 金色顶部装饰 */ +.user-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3rpx; + background: linear-gradient(90deg, transparent 0%, #D4AF37 20%, #F4E6C3 50%, #D4AF37 80%, transparent 100%); + background-size: 200% 100%; + animation: shimmer 4s linear infinite; +} + +@keyframes shimmer { + 0% { + background-position: -200% 0; + } + 100% { + background-position: 200% 0; + } +} + +/* 用户头部 */ +.user-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 24rpx; +} + +.user-basic { + display: flex; + align-items: center; + flex: 1; +} + +.user-avatar { + width: 120rpx; + height: 120rpx; + border-radius: 50%; + margin-right: 16rpx; + background: linear-gradient(135deg, #2A2A2A 0%, #3A3A3A 100%); + border: 3rpx solid #D4AF37; + box-shadow: 0 4rpx 12rpx rgba(212, 175, 55, 0.4); + position: relative; + animation: avatarGlow 3s ease-in-out infinite; +} + +@keyframes avatarGlow { + 0%, 100% { + box-shadow: 0 4rpx 12rpx rgba(212, 175, 55, 0.4); + } + 50% { + box-shadow: 0 4rpx 20rpx rgba(212, 175, 55, 0.6), 0 0 30rpx rgba(212, 175, 55, 0.3); + } +} + +.user-info { + display: flex; + flex-direction: column; + gap: 4rpx; +} + +.user-nickname { + font-size: 32rpx; + font-weight: bold; + color: #FFFFFF; +} + +.login-tip { + font-size: 24rpx; + color: #B0B0B0; +} + +/* 扫码按钮 */ +.scan-btn { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 16rpx 24rpx; + background: #6d552a; + border-radius: 12rpx; + gap: 4rpx; + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.3); +} + +.scan-icon { + font-size: 36rpx; +} + +.scan-text { + font-size: 20rpx; + color: #FFFFFF; + font-weight: 500; +} + +/* 统计数据行 */ +.user-stats { + display: flex; + align-items: center; + justify-content: space-around; + padding-top: 24rpx; + border-top: 1rpx solid #f0f0f0; +} + +.stat-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 8rpx; +} + +.stat-label { + font-size: 24rpx; + color: #B0B0B0; +} + +.stat-value { + font-size: 32rpx; + font-weight: bold; + color: #FFFFFF; +} + +.stat-divider { + width: 1rpx; + height: 40rpx; + background: #e5e5e5; +} + +/* 操作按钮 */ +.action-buttons { + display: flex; + gap: 20rpx; + margin-bottom: 30rpx; +} + +.action-btn { + flex: 1; + height: 100rpx; + border-radius: 16rpx; + display: flex; + align-items: center; + justify-content: center; + gap: 12rpx; + box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08); + transition: all 0.3s ease; +} + +.action-btn:active { + transform: scale(0.98); + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1); +} + +.primary-btn { + background: linear-gradient(135deg, #50C878 0%, #06a854 100%); + box-shadow: 0 8rpx 24rpx rgba(80, 200, 120, 0.4); +} + +.primary-btn:active { + box-shadow: 0 4rpx 16rpx rgba(80, 200, 120, 0.5); +} + +.secondary-btn { + background: linear-gradient(135deg, #C0C0C0 0%, #E5E5E5 100%); + box-shadow: 0 8rpx 24rpx rgba(192, 192, 192, 0.3); +} + +.secondary-btn:active { + box-shadow: 0 4rpx 16rpx rgba(192, 192, 192, 0.4); +} + +.btn-icon { + font-size: 40rpx; + font-weight: bold; + color: #FFFFFF; +} + +.btn-text { + font-size: 28rpx; + font-weight: 500; + color: #FFFFFF; +} + +/* 房间区域 */ +.section { + margin-bottom: 30rpx; +} + +.section-header { + margin-bottom: 20rpx; + padding: 0 8rpx; +} + +.section-title { + font-size: 32rpx; + font-weight: bold; + color: #FFFFFF; +} + +/* 房间网格 - 一行两个 */ +.room-grid { + display: flex; + flex-wrap: wrap; + gap: 20rpx; +} + +.room-card { + flex: 0 0 calc((100% - 100rpx) / 2); + background: #1A1A1A; + border-radius: 20rpx; + padding: 24rpx 20rpx; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.15); + transition: all 0.3s ease; + display: flex; + flex-direction: column; + gap: 12rpx; +} + +.room-card:active { + transform: scale(0.98); + box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.2); +} + +/* 房间标题行 */ +.room-title-row { + display: flex; + justify-content: space-between; + align-items: center; +} + +.room-name { + font-size: 28rpx; + font-weight: bold; + color: #FFFFFF; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex: 1; + margin-right: 8rpx; +} + +.host-badge { + font-size: 20rpx; + padding: 4rpx 12rpx; + border-radius: 6rpx; + background: rgba(255, 154, 118, 0.1); + color: #ff9a76; + white-space: nowrap; + font-weight: 500; +} + +/* 状态和人数行 */ +.room-info-row { + display: flex; + justify-content: space-between; + align-items: center; +} + +.room-status-badge { + font-size: 20rpx; + padding: 4rpx 12rpx; + border-radius: 6rpx; + font-weight: 500; +} + +.room-status-badge.status-playing { + background: rgba(7, 193, 96, 0.1); + color: #50C878; +} + +.room-status-badge.status-waiting { + background: rgba(255, 149, 0, 0.1); + color: #ff9500; +} + +.room-players { + font-size: 24rpx; + color: #B0B0B0; + font-weight: 500; +} + +/* 时间行 */ +.room-time { + padding-top: 8rpx; + border-top: 1rpx solid #f5f5f5; +} + +.time-text { + font-size: 22rpx; + color: #B0B0B0; +} + +/* 空状态 */ +.empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 80rpx 0; + background: #1A1A1A; + border-radius: 20rpx; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.15); + margin-bottom: 20rpx; +} + +.empty-icon { + font-size: 80rpx; + margin-bottom: 20rpx; + opacity: 0.5; +} + +.empty-text { + font-size: 28rpx; + color: #B0B0B0; + margin-bottom: 12rpx; +} + +.empty-hint { + font-size: 24rpx; + color: #ccc; +} diff --git a/pages/login/login.js b/pages/login/login.js new file mode 100644 index 0000000..150eb1b --- /dev/null +++ b/pages/login/login.js @@ -0,0 +1,196 @@ +// pages/login/login.js +import request from '../../utils/request' +import { gatewayHttpClient } from '@jdmini/api' + +Page({ + data: { + nickname: '', + avatarUrl: '', + logging: false, + isEdit: false, // 是否是编辑模式 + statusBarHeight: 0, // 状态栏高度 + navBarHeight: 0 // 导航栏高度 + }, + + onLoad() { + // 获取系统信息,计算导航栏高度 + this.setNavigationBarInfo() + + // 检查是否已登录 + const app = getApp() + const userInfo = app.getUserInfo() + + if (userInfo) { + // 已登录,进入编辑模式,预填用户信息 + this.setData({ + isEdit: true, + nickname: userInfo.nickname, + avatarUrl: userInfo.avatar_url + }) + return + } + + this.CheckBackend() + + }, + + async CheckBackend(){ + const app = getApp() + const userOpenid = app.getUserOpenid() + if (userOpenid){ + const data = await request.get('/auth/profile') + console.log(data) + data.nickname?this.setData({nickname: data.nickname}) :this.setData({nickname: ''}) + this.setData({ + avatarUrl: data.avatar_url + }) + } + }, + + + // 设置导航栏信息 + setNavigationBarInfo() { + const windowInfo = wx.getWindowInfo() + const menuButtonInfo = wx.getMenuButtonBoundingClientRect() + + // 状态栏高度 + const statusBarHeight = windowInfo.statusBarHeight + + // 胶囊按钮的top值,计算返回按钮的top位置 + const menuButtonTop = menuButtonInfo.top + + // 胶囊按钮高度 + const menuButtonHeight = menuButtonInfo.height + + this.setData({ + statusBarHeight: statusBarHeight, + navBarHeight: menuButtonTop, + menuButtonHeight: menuButtonHeight + }) + }, + + // 选择头像 - 使用官方组件回调 + async onChooseAvatar(e) { + const { avatarUrl } = e.detail + const data = await gatewayHttpClient.uploadAvatar(avatarUrl) + wx.setStorageSync('userInfo', { + avatarUrl: data.data + '?v=' + Date.now() + }) + this.setData({ + avatarUrl: data.data + '?v=' + Date.now() + }) + console.log('选择头像:', avatarUrl, data.data) + }, + + // 昵称输入 - 实时更新 + onNicknameInput(e) { + const nickname = e.detail.value + this.setData({ + nickname + }) + console.log('实时输入昵称:', nickname) + }, + + // 昵称失焦 - 使用官方组件 + onNicknameBlur(e) { + const nickname = e.detail.value + this.setData({ + nickname + }) + console.log('昵称失焦:', nickname) + }, + + // 登录或保存修改 + async handleLogin() { + const { nickname, avatarUrl, isEdit } = this.data + const app = getApp() + + // 验证 + if (!nickname || nickname.trim() === '') { + wx.showToast({ + title: '请输入昵称', + icon: 'none' + }) + return + } + + if (!avatarUrl) { + wx.showToast({ + title: '请选择头像', + icon: 'none' + }) + return + } + + // 验证是否有openid + if (!app.globalData.openid) { + wx.showToast({ + title: 'JD登录未完成,请稍后', + icon: 'none' + }) + return + } + + if (this.data.logging) return + + this.setData({ logging: true }) + wx.showLoading({ title: isEdit ? '保存中...' : '登录中...' }) + + try { + // 调用后端登录接口(登录和更新用同一个接口) + const loginData = await request.post('/auth/login', { + openid: app.globalData.openid, + userInfo: { + nickName: nickname.trim(), + avatarUrl: avatarUrl + } + }) + + // 保存用户信息 + wx.setStorageSync('userInfo', loginData.player) + app.globalData.userInfo = loginData.player + + wx.hideLoading() + wx.showToast({ + title: isEdit ? '保存成功' : '登录成功', + icon: 'success', + duration: 1500 + }) + + // 刷新首页数据 + app.refreshIndexPage() + + setTimeout(() => { + // 获取页面栈,判断是否有上一页 + const pages = getCurrentPages() + if (pages.length > 1) { + // 有上一页,返回上一页 + wx.navigateBack({ + delta: 1 + }) + } else { + // 没有上一页,跳转到首页 + wx.redirectTo({ + url: '/pages/index/index' + }) + } + }, 1500) + + } catch (error) { + wx.hideLoading() + this.setData({ logging: false }) + + wx.showToast({ + title: error.message || (isEdit ? '保存失败' : '登录失败'), + icon: 'none' + }) + } + }, + + // 返回上一页 + handleBack() { + wx.navigateBack({ + delta: 1 + }) + } +}) diff --git a/pages/login/login.json b/pages/login/login.json new file mode 100644 index 0000000..ecf067c --- /dev/null +++ b/pages/login/login.json @@ -0,0 +1,6 @@ +{ + "navigationStyle": "custom", + "usingComponents": { + "custom-navbar": "../../components/navbar/navbar" + } +} diff --git a/pages/login/login.wxml b/pages/login/login.wxml new file mode 100644 index 0000000..fcb4894 --- /dev/null +++ b/pages/login/login.wxml @@ -0,0 +1,51 @@ + + + diff --git a/pages/login/login.wxss b/pages/login/login.wxss new file mode 100644 index 0000000..0ddbe97 --- /dev/null +++ b/pages/login/login.wxss @@ -0,0 +1,182 @@ +/* 登录页面样式 */ +page { + background: linear-gradient(135deg, #1A1A1A 0%, #252525 100%); + min-height: 100vh; +} + +.login-container { + padding: 60rpx 40rpx; + box-sizing: border-box; +} + +/* Logo区域 */ +.logo-section { + text-align: center; + margin-bottom: 80rpx; + padding-top: 120rpx; +} + +.logo-icon { + font-size: 120rpx; + margin-bottom: 20rpx; + animation: float 3s ease-in-out infinite; +} + +@keyframes float { + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-10rpx); + } +} + +.app-name { + display: block; + font-size: 48rpx; + font-weight: bold; + color: white; + margin-bottom: 16rpx; +} + +.app-slogan { + display: block; + font-size: 28rpx; + color: rgba(255, 255, 255, 0.8); +} + +/* 表单区域 */ +.form-section { + background: #1A1A1A; + border-radius: 20rpx; + padding: 40rpx 32rpx; + margin-bottom: 40rpx; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.15); +} + +.form-item { + margin-bottom: 40rpx; +} + +.form-item:last-child { + margin-bottom: 0; +} + +.form-label { + display: block; + font-size: 28rpx; + font-weight: 600; + color: #FFFFFF; + margin-bottom: 16rpx; +} + +/* 头像选择器 - 容器 */ +.avatar-wrapper { + position: relative; + width: 200rpx; + height: 200rpx; + margin: 0 auto 20rpx; + padding: 0; + background: transparent; + border: none; +} + +/* 移除 button 默认样式 */ +.avatar-wrapper::after, +.avatar-wrapper::before { + display: none; +} + +/* 头像图片 */ +.login-container .avatar { + width: 200rpx; + height: 200rpx; + border-radius: 50%; + border: 4rpx solid #1A1A1A; + box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.3); + display: block; + margin: 0 auto; +} + +/* 遮罩层 */ +.avatar-mask { + position: absolute; + bottom: 4rpx; + left: 50%; + transform: translateX(-50%); + width: 180rpx; + height: 50rpx; + background: rgba(0, 0, 0, 0.6); + border-radius: 0 0 96rpx 96rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.avatar-text { + color: white; + font-size: 24rpx; +} + +/* 昵称输入 */ +.nickname-input { + width: 100%; + height: 88rpx; + padding: 0 20rpx; + background: #f8f9fa; + border: 2rpx solid rgba(0, 0, 0, 0.1); + border-radius: 12rpx; + font-size: 30rpx; + color: #FFFFFF; + box-sizing: border-box; + line-height: 88rpx; + transition: all 0.3s; +} + +.nickname-input:focus { + border-color: #1A1A1A; + background: #1A1A1A; + box-shadow: 0 0 0 4rpx rgba(0, 0, 0, 0.1); +} + +.input-hint { + display: block; + font-size: 24rpx; + color: #B0B0B0; + margin-top: 12rpx; +} + +/* 登录区域 */ +.login-section { + text-align: center; +} + +.login-btn { + width: 100%; + padding: 28rpx; + background: linear-gradient(135deg, #50C878 0%, #06a854 100%); + color: white; + border-radius: 16rpx; + font-size: 32rpx; + font-weight: bold; + border: none; + box-shadow: 0 8rpx 24rpx rgba(7, 193, 96, 0.4); + margin-bottom: 24rpx; + transition: all 0.3s; +} + +.login-btn:active { + opacity: 0.9; + transform: scale(0.98); +} + +.login-btn[disabled] { + background: #e0e0e0; + color: #B0B0B0; + box-shadow: none; +} + +.terms-text { + font-size: 24rpx; + color: rgba(255, 255, 255, 0.8); +} diff --git a/pages/profile/index/index.js b/pages/profile/index/index.js new file mode 100644 index 0000000..88f98d8 --- /dev/null +++ b/pages/profile/index/index.js @@ -0,0 +1,187 @@ +// pages/profile/index/index.js +import request from '../../../utils/request' + +Page({ + data: { + userInfo: null, + isLogin: false, + navbarHeight: 0, + stats: { + total_games: 0, + total_win: 0, + total_loss: 0, + win_rate: 0, + net_profit: 0 + }, + recentSessions: [], + achievements: [] + }, + + onLoad() { + // 计算导航栏高度 + const windowInfo = wx.getWindowInfo() + const menuButtonInfo = wx.getMenuButtonBoundingClientRect() + const statusBarHeight = windowInfo.statusBarHeight + const menuButtonTop = menuButtonInfo.top + const navbarHeight = menuButtonInfo.bottom + (menuButtonTop - statusBarHeight) + this.setData({ navbarHeight }) + + this.loadUserData() + }, + + onShow() { + // 每次显示时刷新数据 + this.loadUserData() + }, + + // 加载用户数据 + async loadUserData() { + const app = getApp() + const userInfo = app.getUserInfo() + + if (userInfo) { + this.setData({ + userInfo, + isLogin: true + }) + + await this.loadUserStats() + await this.loadRecentSessions() + this.calculateAchievements() + } else { + this.setData({ + isLogin: false + }) + } + }, + + // 加载用户统计 + async loadUserStats() { + try { + const stats = await request.get('/auth/profile') + + // 计算净盈利 + const netProfit = (stats.total_win || 0) - (stats.total_loss || 0) + + this.setData({ + stats: { + total_games: stats.total_games || 0, + total_win: stats.total_win || 0, + total_loss: stats.total_loss || 0, + win_rate: stats.win_rate || 0, + net_profit: netProfit + } + }) + } catch (error) { + console.error('加载统计失败:', error) + } + }, + + // 加载最近牌局 + async loadRecentSessions() { + try { + const data = await request.get('/rooms/my-sessions', { + page: 1, + pageSize: 5 + }) + + this.setData({ + recentSessions: data.list || [] + }) + } catch (error) { + console.error('加载牌局失败:', error) + } + }, + + // 计算成就 + calculateAchievements() { + const { stats } = this.data + const achievements = [] + + // 根据游戏场次 + if (stats.total_games >= 100) { + achievements.push({ icon: '🏆', name: '百战老将', desc: '参与100场游戏' }) + } else if (stats.total_games >= 50) { + achievements.push({ icon: '🎖️', name: '资深玩家', desc: '参与50场游戏' }) + } else if (stats.total_games >= 10) { + achievements.push({ icon: '🌟', name: '初出茅庐', desc: '参与10场游戏' }) + } + + // 根据胜率 + if (stats.win_rate >= 70) { + achievements.push({ icon: '👑', name: '牌桌霸主', desc: '胜率超过70%' }) + } else if (stats.win_rate >= 60) { + achievements.push({ icon: '💎', name: '高手', desc: '胜率超过60%' }) + } + + // 根据盈利 + if (stats.net_profit > 10000) { + achievements.push({ icon: '💰', name: '财运亨通', desc: '累计盈利过万' }) + } else if (stats.net_profit > 5000) { + achievements.push({ icon: '💵', name: '小有盈余', desc: '累计盈利5000+' }) + } + + this.setData({ achievements }) + }, + + // 处理登录 + handleLogin() { + wx.navigateTo({ + url: '/pages/login/login' + }) + }, + + // 编辑资料 + editProfile() { + wx.navigateTo({ + url: '/pages/login/login' + }) + }, + + // 查看个人统计 + viewStats() { + wx.navigateTo({ + url: '/pages/stats/personal/personal' + }) + }, + + // 查看全部牌局 + viewAllSessions() { + wx.navigateTo({ + url: '/pages/record/list/list' + }) + }, + + // 进入牌局 + goToSession(e) { + const id = e.currentTarget.dataset.id + wx.navigateTo({ + url: `/pages/game/detail/detail?id=${id}` + }) + }, + + // 关于 + showAbout() { + wx.showModal({ + title: '关于打牌记账', + content: '版本:v1.0.0\n\n一款专业的打牌记账小程序\n支持麻将、扑克等多种游戏类型\n实时记录、智能统计、便捷结算', + showCancel: false + }) + }, + + // 联系客服 + contactService() { + wx.showModal({ + title: '联系客服', + content: '客服微信:请添加公众号\n客服电话:暂未开通', + showCancel: false + }) + }, + + // 下拉刷新 + onPullDownRefresh() { + this.loadUserData().then(() => { + wx.stopPullDownRefresh() + }) + } +}) \ No newline at end of file diff --git a/pages/profile/index/index.json b/pages/profile/index/index.json new file mode 100644 index 0000000..903d8a9 --- /dev/null +++ b/pages/profile/index/index.json @@ -0,0 +1,8 @@ +{ + "navigationStyle": "custom", + "usingComponents": { + "jdwx-link": "@jdmini/components/jdwx-link", + "custom-tabbar": "../../../components/custom-tabbar/custom-tabbar", + "custom-navbar": "../../../components/navbar/navbar" + } +} diff --git a/pages/profile/index/index.wxml b/pages/profile/index/index.wxml new file mode 100644 index 0000000..957cc77 --- /dev/null +++ b/pages/profile/index/index.wxml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + 📝 + 全部牌局 + > + + + + ℹ️ + 关于我们 + > + + + + + + + + + 推荐小程序 + > + + + + + + + + + diff --git a/pages/profile/index/index.wxss b/pages/profile/index/index.wxss new file mode 100644 index 0000000..a847774 --- /dev/null +++ b/pages/profile/index/index.wxss @@ -0,0 +1,270 @@ +/* 个人中心页面样式 */ +page { + background: linear-gradient(135deg, #1A1A1A 0%, #252525 100%); + min-height: 100vh; +} + +.container { + padding: 30rpx; + padding-bottom: 120rpx; +} + +/* 用户信息头部 */ +.profile-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20rpx; + padding: 32rpx 24rpx; +} + +.user-info { + display: flex; + align-items: center; + gap: 20rpx; + flex: 1; +} + +.avatar-large { + width: 120rpx; + height: 120rpx; + border-radius: 50%; + border: 4rpx solid #fff; + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1); +} + +.user-detail { + display: flex; + flex-direction: column; + gap: 8rpx; +} + +.nickname { + font-size: 36rpx; + font-weight: bold; + color: #FFFFFF; +} + +.user-id { + font-size: 24rpx; + color: #B0B0B0; +} + +.btn-edit { + padding: 16rpx 32rpx; + background: #f8f8f8; + color: #FFFFFF; + border-radius: 40rpx; + font-size: 28rpx; + border: none; +} + +/* 数据统计卡片 */ +.stats-card { + margin-bottom: 20rpx; +} + +.stats-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20rpx; + margin: 20rpx 0; +} + +.stat-box { + display: flex; + flex-direction: column; + align-items: center; + padding: 24rpx; + background: #f8f9fa; + border-radius: 12rpx; + gap: 8rpx; +} + +.stat-number { + font-size: 40rpx; + font-weight: bold; + color: #FFFFFF; +} + +.stat-label { + font-size: 24rpx; + color: #B0B0B0; +} + +.view-more-btn { + width: 100%; + padding: 24rpx; + background: linear-gradient(135deg, #50C878 0%, #06a854 100%); + color: white; + border-radius: 12rpx; + font-size: 30rpx; + font-weight: 500; + border: none; + margin-top: 8rpx; +} + +/* 成就徽章 */ +.achievements-card { + margin-bottom: 20rpx; +} + +.achievements-list { + display: flex; + flex-direction: column; + gap: 16rpx; + margin-top: 20rpx; +} + +.achievement-item { + display: flex; + align-items: center; + gap: 16rpx; + padding: 20rpx; + background: #f8f9fa; + border-radius: 12rpx; +} + +.achievement-icon { + font-size: 48rpx; + width: 64rpx; + height: 64rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.achievement-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 4rpx; +} + +.achievement-name { + font-size: 30rpx; + font-weight: bold; + color: #FFFFFF; +} + +.achievement-desc { + font-size: 24rpx; + color: #B0B0B0; +} + +/* 最近牌局 */ +.recent-sessions-card { + margin-bottom: 20rpx; +} + +.more-link { + font-size: 24rpx; + color: #50C878; +} + +.sessions-list { + display: flex; + flex-direction: column; + gap: 12rpx; + margin-top: 20rpx; +} + +.session-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20rpx; + background: #f8f9fa; + border-radius: 12rpx; +} + +.session-item:active { + background: #e8e8e8; +} + +.session-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 8rpx; +} + +.session-name { + font-size: 30rpx; + font-weight: 500; + color: #FFFFFF; +} + +.session-meta { + font-size: 24rpx; + color: #B0B0B0; +} + +.session-status { + padding: 8rpx 16rpx; + border-radius: 20rpx; + font-size: 24rpx; + font-weight: 500; +} + +.status-playing { + background: #e8f5e9; + color: #50C878; +} + +.status-waiting { + background: #1A1A1A3e0; + color: #ff9500; +} + +.status-finished { + background: #f5f5f5; + color: #B0B0B0; +} + +/* 功能菜单 */ +.menu-card { + margin-bottom: 20rpx; +} + +.menu-list { + display: flex; + flex-direction: column; +} + +.menu-item { + display: flex; + align-items: center; + padding: 24rpx 0; + border-bottom: 1rpx solid #f0f0f0; + gap: 16rpx; +} + +.menu-item:last-child { + border-bottom: none; +} + +.menu-item:active { + background: #f8f8f8; +} + +.menu-icon { + font-size: 40rpx; + width: 48rpx; + text-align: center; +} + +.menu-text { + flex: 1; + font-size: 30rpx; + color: #FFFFFF; +} + +.menu-arrow { + font-size: 24rpx; + color: #B0B0B0; +} +/* 为TabBar预留空间 */ +.container { + padding-bottom: 120rpx; +} diff --git a/pages/record/list/list.js b/pages/record/list/list.js new file mode 100644 index 0000000..4bc00d0 --- /dev/null +++ b/pages/record/list/list.js @@ -0,0 +1,149 @@ +// pages/record/list/list.js +import request from '../../../utils/request' + +Page({ + data: { + sessions: [], + loading: false, + page: 1, + pageSize: 10, + hasMore: true, + navbarHeight: 0, + + // 统计数据 + totalSessions: 0, + totalGames: 0, + totalProfit: 0, + + // 游戏类型文本映射 + gameTypeText: { + 'mahjong': '麻将', + 'poker': '扑克', + 'other': '其他' + } + }, + + onLoad() { + // 计算导航栏高度 + const windowInfo = wx.getWindowInfo() + const menuButtonInfo = wx.getMenuButtonBoundingClientRect() + const statusBarHeight = windowInfo.statusBarHeight + const menuButtonTop = menuButtonInfo.top + const navbarHeight = menuButtonInfo.bottom + (menuButtonTop - statusBarHeight) + this.setData({ navbarHeight }) + + this.loadSessions() + this.loadStatistics() + }, + + // 加载牌局列表 + async loadSessions(isRefresh = false) { + if (this.data.loading) return + + if (isRefresh) { + this.setData({ + page: 1, + sessions: [], + hasMore: true + }) + } + + this.setData({ loading: true }) + + try { + const data = await request.get('/rooms/my-sessions', { + page: this.data.page, + pageSize: this.data.pageSize + }) + + const newSessions = (data.list || []).map(session => { + // 格式化时间 + const createTime = new Date(session.created_at * 1000) + const now = new Date() + const diffDays = Math.floor((now - createTime) / (1000 * 60 * 60 * 24)) + + let timeText = '' + if (diffDays === 0) { + timeText = '今天' + } else if (diffDays === 1) { + timeText = '昨天' + } else if (diffDays < 7) { + timeText = `${diffDays}天前` + } else { + timeText = `${createTime.getMonth() + 1}/${createTime.getDate()}` + } + + // 格式化其他玩家昵称显示 + let otherPlayersText = '' + if (session.other_players && session.other_players.length > 0) { + otherPlayersText = session.other_players.join('、') + } else { + otherPlayersText = '无其他玩家' + } + + return { + ...session, + timeText, + otherPlayersText, + gameTypeName: this.data.gameTypeText[session.game_type] || session.game_type, + profitText: session.my_win_loss > 0 ? `+${session.my_win_loss}` : session.my_win_loss + } + }) + + this.setData({ + sessions: isRefresh ? newSessions : [...this.data.sessions, ...newSessions], + hasMore: newSessions.length >= this.data.pageSize, + loading: false + }) + } catch (error) { + console.error('加载牌局失败:', error) + this.setData({ loading: false }) + wx.showToast({ + title: '加载失败', + icon: 'none' + }) + } + }, + + // 加载统计数据 + async loadStatistics() { + try { + const stats = await request.get('/auth/profile') + + const totalProfit = (stats.total_win || 0) - (stats.total_loss || 0) + + this.setData({ + totalSessions: stats.total_games || 0, + totalGames: stats.total_games || 0, + totalProfit + }) + } catch (error) { + console.error('加载统计失败:', error) + } + }, + + // 进入牌局详情 + goToSession(e) { + const id = e.currentTarget.dataset.id + wx.navigateTo({ + url: `/pages/game/detail/detail?id=${id}` + }) + }, + + + // 下拉刷新 + onPullDownRefresh() { + this.loadSessions(true).then(() => { + this.loadStatistics() + wx.stopPullDownRefresh() + }) + }, + + // 上拉加载更多 + onReachBottom() { + if (this.data.hasMore && !this.data.loading) { + this.setData({ page: this.data.page + 1 }) + this.loadSessions() + } + } +}) \ No newline at end of file diff --git a/pages/record/list/list.json b/pages/record/list/list.json new file mode 100644 index 0000000..c202186 --- /dev/null +++ b/pages/record/list/list.json @@ -0,0 +1,6 @@ +{ + "navigationStyle": "custom", + "usingComponents": { + "custom-navbar": "../../../components/navbar/navbar" + } +} \ No newline at end of file diff --git a/pages/record/list/list.wxml b/pages/record/list/list.wxml new file mode 100644 index 0000000..269d206 --- /dev/null +++ b/pages/record/list/list.wxml @@ -0,0 +1,60 @@ + + + + + + + {{totalSessions}} + 参与场次 + + + + + {{totalProfit > 0 ? '+' : ''}}{{totalProfit}} + + 累计盈亏 + + + + + + + + + {{item.room_name}} + + {{item.gameTypeName}} + {{item.timeText}} + + + + + + {{item.otherPlayersText}} + + 我的盈亏 + + {{item.profitText || 0}} + + + + + + + + + 加载中... + + + + + 没有更多了 + + + + + 📝 + 暂无战绩记录 + 快去创建或加入牌局吧 + + \ No newline at end of file diff --git a/pages/record/list/list.wxss b/pages/record/list/list.wxss new file mode 100644 index 0000000..4c122df --- /dev/null +++ b/pages/record/list/list.wxss @@ -0,0 +1,179 @@ +/* 战绩列表页面样式 */ +page { + background: linear-gradient(135deg, #1A1A1A 0%, #252525 100%); + min-height: 100vh; +} + +.container { + padding: 30rpx; + padding-bottom: 120rpx; +} + +/* 统计概览 */ +.stats-overview { + display: flex; + justify-content: space-around; + align-items: center; + padding: 32rpx 24rpx; + margin-bottom: 20rpx; +} + +.overview-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 8rpx; +} + +.overview-number { + font-size: 44rpx; + font-weight: bold; + color: #FFFFFF; +} + +.overview-number.win { + color: #50C878; +} + +.overview-number.lose { + color: #ff3b30; +} + +.overview-label { + font-size: 24rpx; + color: #B0B0B0; +} + +.divider-vertical { + width: 1rpx; + height: 60rpx; + background: #e5e5e5; +} + +/* 牌局列表 */ +.sessions-list { + display: flex; + flex-direction: column; + gap: 16rpx; +} + +.session-card { + padding: 24rpx; +} + +/* 第一行:房间名 + 类型 + 时间 */ +.session-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16rpx; +} + +.session-name { + font-size: 32rpx; + font-weight: bold; + color: #FFFFFF; + flex: 1; +} + +.session-right { + display: flex; + align-items: center; + gap: 16rpx; +} + +.game-type { + font-size: 26rpx; + color: #E0E0E0; + padding: 4rpx 12rpx; + background: #f0f0f0; + border-radius: 8rpx; +} + +.session-time { + font-size: 24rpx; + color: #B0B0B0; +} + +/* 第二行:其他玩家 + 我的盈亏 */ +.session-body { + display: flex; + justify-content: space-between; + align-items: center; +} + +.player-names { + font-size: 28rpx; + color: #E0E0E0; + flex: 1; + margin-right: 16rpx; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.profit-section { + display: flex; + align-items: center; + gap: 12rpx; +} + +.profit-label { + font-size: 24rpx; + color: #B0B0B0; +} + +.profit-value { + font-size: 36rpx; + font-weight: bold; + color: #FFFFFF; +} + +.profit-value.win { + color: #50C878; +} + +.profit-value.lose { + color: #ff3b30; +} + +/* 加载状态 */ +.loading-more { + padding: 40rpx 0; + text-align: center; + font-size: 28rpx; + color: #B0B0B0; +} + +.no-more { + padding: 40rpx 0; + text-align: center; + font-size: 24rpx; + color: #ccc; +} + +/* 空状态 */ +.empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 120rpx 40rpx; +} + +.empty-icon { + font-size: 120rpx; + margin-bottom: 24rpx; + opacity: 0.5; +} + +.empty-text { + font-size: 32rpx; + color: #B0B0B0; + margin-bottom: 12rpx; +} + +.empty-hint { + font-size: 26rpx; + color: #ccc; +} \ No newline at end of file diff --git a/pages/stats/personal/personal-empty.wxml b/pages/stats/personal/personal-empty.wxml new file mode 100644 index 0000000..ad17212 --- /dev/null +++ b/pages/stats/personal/personal-empty.wxml @@ -0,0 +1,6 @@ + + + 📊 + 登录后查看统计数据 + + diff --git a/pages/stats/personal/personal.js b/pages/stats/personal/personal.js new file mode 100644 index 0000000..b740759 --- /dev/null +++ b/pages/stats/personal/personal.js @@ -0,0 +1,266 @@ +// pages/stats/personal/personal.js +import request from '../../../utils/request' + +Page({ + data: { + userInfo: null, + loading: true, + navbarHeight: 0, + + // 统计数据 + stats: { + total_games: 0, + total_win: 0, + total_loss: 0, + win_rate: 0, + net_profit: 0, + max_win: 0, + max_loss: 0, + avg_profit: 0 + }, + + // 时间范围 + timeRange: 'all', // all/month/week + timeRangeText: { + all: '全部', + month: '近30天', + week: '近7天' + }, + + // 趋势数据 + trendData: [], + + // 游戏类型统计 + gameTypeStats: [], + + // 最佳战绩 + bestRecords: { + max_win_session: null, + max_loss_session: null, + longest_win_streak: 0, + longest_loss_streak: 0 + } + }, + + _isFirstLoad: true, // 标记是否首次加载 + + onLoad(options) { + this._isFirstLoad = true + + // 计算导航栏高度 + const windowInfo = wx.getWindowInfo() + const menuButtonInfo = wx.getMenuButtonBoundingClientRect() + const statusBarHeight = windowInfo.statusBarHeight + const menuButtonTop = menuButtonInfo.top + const navbarHeight = menuButtonInfo.bottom + (menuButtonTop - statusBarHeight) + this.setData({ navbarHeight }) + + this.loadUserInfo() + }, + + onShow() { + // 只在非首次加载时刷新数据(避免onLoad和onShow重复加载) + if (!this._isFirstLoad) { + this.loadUserInfo() + } + this._isFirstLoad = false + }, + + // 加载用户信息 + loadUserInfo() { + const app = getApp() + const userInfo = app.getUserInfo() + + if (userInfo) { + this.setData({ userInfo }) + this.loadStats() + this.loadTrendData() + this.loadGameTypeStats() + } else { + // 未登录,显示提示 + this.setData({ + userInfo: null, + loading: false + }) + } + }, + + // 跳转登录 + goToLogin() { + wx.navigateTo({ + url: '/pages/login/login' + }) + }, + + // 加载统计数据 + async loadStats() { + this.setData({ loading: true }) + + try { + // 根据时间范围确定天数 + let days = 0 // 0表示全部 + if (this.data.timeRange === 'month') { + days = 30 + } else if (this.data.timeRange === 'week') { + days = 7 + } + + const stats = await request.get('/auth/profile', { days }) + + // 计算额外统计 + const netProfit = (stats.total_win || 0) - (stats.total_loss || 0) + const avgProfit = stats.total_games > 0 + ? Math.round(netProfit / stats.total_games) + : 0 + + this.setData({ + stats: { + total_games: stats.total_games || 0, + total_win: stats.total_win || 0, + total_loss: stats.total_loss || 0, + win_rate: stats.win_rate || 0, + net_profit: netProfit, + max_win: stats.max_win || 0, + max_loss: stats.max_loss || 0, + avg_profit: avgProfit + }, + loading: false + }) + } catch (error) { + console.error('加载统计失败:', error) + this.setData({ loading: false }) + wx.showToast({ + title: '加载失败', + icon: 'none' + }) + } + }, + + // 加载趋势数据 + async loadTrendData() { + try { + // 根据时间范围确定天数 + let days = 30 + if (this.data.timeRange === 'month') { + days = 30 + } else if (this.data.timeRange === 'week') { + days = 7 + } else if (this.data.timeRange === 'all') { + days = 365 // 全部显示一年数据 + } + + const data = await request.get('/stats/trend', { days }) + + console.log('=== 趋势数据调试 ===') + console.log('API返回数据:', JSON.stringify(data)) + console.log('数据长度:', data?.length) + console.log('数据类型:', typeof data) + + if (!data || data.length === 0) { + console.log('数据为空,设置trendData为[]') + this.setData({ trendData: [] }) + return + } + + // 找出最大赢和最大输 + const maxWin = Math.max(...data.map(item => item.profit > 0 ? item.profit : 0), 0) + const maxLoss = Math.abs(Math.min(...data.map(item => item.profit < 0 ? item.profit : 0), 0)) + const maxValue = Math.max(maxWin, maxLoss, 1) + + console.log('maxWin:', maxWin, 'maxLoss:', maxLoss, 'maxValue:', maxValue) + + // 格式化日期和计算百分比 + const formattedData = data.map(item => { + const date = new Date(item.date) + const profit = item.profit || 0 + // 计算相对于最大值的百分比 + const heightPercent = (Math.abs(profit) / maxValue * 50).toFixed(2) + + console.log(`处理数据: date=${item.date}, profit=${profit}, heightPercent=${heightPercent}`) + + return { + ...item, + date: `${date.getMonth() + 1}/${date.getDate()}`, + heightPercent: heightPercent + } + }) + + // 计算Y轴刻度 (上方正数,下方负数) + const yAxisLabels = [ + Math.round(maxValue), + Math.round(maxValue * 0.5), + 0, + -Math.round(maxValue * 0.5), + -Math.round(maxValue) + ] + + console.log('formattedData:', JSON.stringify(formattedData)) + console.log('yAxisLabels:', yAxisLabels) + + this.setData({ + trendData: formattedData, + maxValue: maxValue, + yAxisLabels: yAxisLabels + }) + + console.log('setData完成') + } catch (error) { + console.error('加载趋势失败:', error) + this.setData({ trendData: [] }) + } + }, + + // 加载游戏类型统计 + async loadGameTypeStats() { + try { + const data = await request.get('/stats/by-game-type') + // 转换API数据格式为页面需要的格式 + const formattedData = (data || []).map(item => ({ + type: item.game_type, + name: item.game_type_name, + games: item.games, + win_rate: item.win_rate, + profit: item.total_profit + })) + this.setData({ gameTypeStats: formattedData }) + } catch (error) { + console.error('加载游戏类型统计失败:', error) + } + }, + + // 切换时间范围 + onTimeRangeChange(e) { + const timeRange = e.currentTarget.dataset.range + console.log('=== 切换时间范围 ===', timeRange) + this.setData({ timeRange }) + this.loadStats() + this.loadTrendData() + }, + + // 查看战绩列表 + viewRecords() { + wx.navigateTo({ + url: '/pages/record/list/list' + }) + }, + + // 分享战绩 + shareStats() { + wx.showModal({ + title: '功能开发中', + content: '战绩分享功能正在开发中', + showCancel: false + }) + }, + + // 下拉刷新 + onPullDownRefresh() { + Promise.all([ + this.loadStats(), + this.loadTrendData(), + this.loadGameTypeStats() + ]).then(() => { + wx.stopPullDownRefresh() + }) + } +}) \ No newline at end of file diff --git a/pages/stats/personal/personal.json b/pages/stats/personal/personal.json new file mode 100644 index 0000000..d903ba6 --- /dev/null +++ b/pages/stats/personal/personal.json @@ -0,0 +1,7 @@ +{ + "navigationStyle": "custom", + "usingComponents": { + "custom-tabbar": "../../../components/custom-tabbar/custom-tabbar", + "custom-navbar": "../../../components/navbar/navbar" + } +} \ No newline at end of file diff --git a/pages/stats/personal/personal.wxml b/pages/stats/personal/personal.wxml new file mode 100644 index 0000000..17f6ca0 --- /dev/null +++ b/pages/stats/personal/personal.wxml @@ -0,0 +1,168 @@ + + + + + + 📊 + 登录后查看统计数据 + + + + + + + + + 全部 + + + 近30天 + + + 近7天 + + + + + + + + + {{stats.net_profit > 0 ? '+' : ''}}{{stats.net_profit}} + + 净盈亏 + + + {{stats.win_rate}}% + 胜率 + + + + + + + + {{stats.total_games}} + 总场次 + + + {{stats.total_win}} + 累计赢 + + + {{stats.total_loss}} + 累计输 + + + {{stats.avg_profit}} + 场均 + + + + + + + 最佳记录 + + + 🏆 + + 单场最大赢 + +{{stats.max_win || 0}} + + + + 💔 + + 单场最大输 + {{stats.max_loss || 0}} + + + + + + + + 游戏类型统计 + + + + {{item.name}} + {{item.games}}场 + + + + 胜率 + {{item.win_rate}}% + + + 盈亏 + + {{item.profit > 0 ? '+' : ''}}{{item.profit}} + + + + + + + + + + 盈亏趋势 + + + 累计盈亏 + + {{trendData[trendData.length - 1].cumulative > 0 ? '+' : ''}}{{trendData[trendData.length - 1].cumulative}} + + + + 统计天数 + {{trendData.length}}天 + + + + + + + {{item}} + + + + + + + + + + + + + + + + + + + {{item.date}} + + + + + + + + + + 查看全部战绩 + + + + + + diff --git a/pages/stats/personal/personal.wxss b/pages/stats/personal/personal.wxss new file mode 100644 index 0000000..9d2825b --- /dev/null +++ b/pages/stats/personal/personal.wxss @@ -0,0 +1,417 @@ +/* 个人统计页面样式 */ +page { + background: linear-gradient(135deg, #1A1A1A 0%, #252525 100%); + min-height: 100vh; +} + +.container { + padding: 30rpx; + padding-bottom: 120rpx; +} + +/* 用户头部 */ +.user-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 32rpx 24rpx; + margin-bottom: 20rpx; +} + +.user-info { + display: flex; + align-items: center; + gap: 16rpx; + flex: 1; +} + +.avatar { + width: 96rpx; + height: 96rpx; + border-radius: 50%; + border: 3rpx solid #fff; + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1); +} + +.user-detail { + display: flex; + flex-direction: column; + gap: 6rpx; +} + +.nickname { + font-size: 32rpx; + font-weight: bold; + color: #FFFFFF; +} + +.user-desc { + font-size: 24rpx; + color: #B0B0B0; +} + +.share-btn { + padding: 16rpx 28rpx; + background: linear-gradient(135deg, #ff9500 0%, #ff7700 100%); + color: white; + border-radius: 40rpx; + font-size: 26rpx; + border: none; +} + +/* 时间范围选择 */ +.time-range-bar { + display: flex; + gap: 12rpx; + padding: 16rpx 24rpx; + margin-bottom: 20rpx; +} + +.range-btn { + flex: 1; + padding: 16rpx 0; + background: #f8f8f8; + border-radius: 24rpx; + font-size: 28rpx; + color: #E0E0E0; + text-align: center; +} + +.range-btn.active { + background: linear-gradient(135deg, #50C878 0%, #06a854 100%); + color: white; + font-weight: bold; +} + +/* 核心数据 */ +.core-stats { + padding: 32rpx 24rpx; + margin-bottom: 20rpx; +} + +.stats-row { + display: flex; + justify-content: space-around; + align-items: baseline; + padding-bottom: 24rpx; +} + +.stat-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 8rpx; + flex: 1; +} + +.stat-item.large { + padding: 0 20rpx; +} + +.stat-number { + font-size: 36rpx; + font-weight: bold; + color: #FFFFFF; + line-height: 1.2; +} + +.stat-number.win { + color: #50C878; +} + +.stat-number.lose { + color: #ff3b30; +} + +.stat-item.large .stat-number { + font-size: 52rpx; + line-height: 1.2; +} + +.stat-label { + font-size: 24rpx; + color: #B0B0B0; +} + +.stats-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 20rpx; + padding-top: 24rpx; +} + +/* 最佳记录 */ +.best-records { + padding: 24rpx; + margin-bottom: 20rpx; +} + +.records-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16rpx; + margin-top: 20rpx; +} + +.record-item { + display: flex; + align-items: center; + gap: 12rpx; + padding: 20rpx; + background: #f8f9fa; + border-radius: 12rpx; +} + +.record-icon { + font-size: 40rpx; +} + +.record-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 6rpx; +} + +.record-label { + font-size: 24rpx; + color: #B0B0B0; +} + +.record-value { + font-size: 32rpx; + font-weight: bold; + color: #FFFFFF; +} + +/* 游戏类型统计 */ +.game-type-stats { + padding: 24rpx; + margin-bottom: 20rpx; +} + +.game-types-list { + display: flex; + flex-direction: column; + gap: 16rpx; + margin-top: 20rpx; +} + +.game-type-item { + padding: 20rpx; + background: #f8f9fa; + border-radius: 12rpx; +} + +.type-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16rpx; +} + +.type-name { + font-size: 30rpx; + font-weight: bold; + color: #FFFFFF; +} + +.type-games { + font-size: 24rpx; + color: #B0B0B0; +} + +.type-stats { + display: flex; + gap: 32rpx; +} + +.type-stat { + flex: 1; + display: flex; + justify-content: space-between; + align-items: center; + padding: 12rpx 16rpx; + background: #1A1A1A; + border-radius: 8rpx; +} + +.type-stat .stat-label { + font-size: 24rpx; + color: #B0B0B0; +} + +.type-stat .stat-value { + font-size: 28rpx; + font-weight: bold; + color: #FFFFFF; +} + +/* 趋势图 */ +.trend-chart { + padding: 24rpx; + margin-bottom: 20rpx; +} + +.chart-summary { + display: flex; + justify-content: space-around; + padding: 20rpx 0; + margin-bottom: 20rpx; + border-bottom: 1rpx solid #f0f0f0; +} + +.summary-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 8rpx; +} + +.summary-label { + font-size: 24rpx; + color: #B0B0B0; +} + +.summary-value { + font-size: 32rpx; + font-weight: bold; + color: #FFFFFF; +} + +.summary-value.win { + color: #50C878; +} + +.summary-value.lose { + color: #ff3b30; +} + +.chart-container { + background: #f8f9fa; + border-radius: 12rpx; + padding: 20rpx 12rpx; + overflow-x: auto; +} + +.chart-wrapper { + display: flex; + gap: 8rpx; +} + +.chart-y-axis { + display: flex; + flex-direction: column; + justify-content: space-between; + height: 280rpx; + padding: 20rpx 0 50rpx; + min-width: 50rpx; +} + +.y-axis-label { + font-size: 20rpx; + color: #B0B0B0; + text-align: right; + line-height: 1; +} + +.chart-bars { + flex: 1; + display: flex; + align-items: center; + justify-content: space-around; + height: 280rpx; + padding: 20rpx 8rpx 0; +} + +.bar-item { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + max-width: 100rpx; + height: 100%; +} + +.bar-column { + flex: 1; + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 12rpx; +} + +.bar-upper { + flex: 1; + width: 100%; + display: flex; + align-items: flex-end; + justify-content: center; +} + +.bar-baseline { + width: 100%; + height: 2rpx; + background: #ddd; + position: relative; +} + +.bar-lower { + flex: 1; + width: 100%; + display: flex; + align-items: flex-start; + justify-content: center; +} + +.bar { + width: 40rpx; + min-height: 8rpx; +} + +.bar-win { + background: linear-gradient(180deg, #06d66f 0%, #50C878 100%); + box-shadow: 0 2rpx 8rpx rgba(7, 193, 96, 0.3); + border-radius: 6rpx 6rpx 0 0; +} + +.bar-lose { + background: linear-gradient(0deg, #ff5449 0%, #ff3b30 100%); + box-shadow: 0 2rpx 8rpx rgba(255, 59, 48, 0.3); + border-radius: 0 0 6rpx 6rpx; +} + +.bar-label { + font-size: 22rpx; + color: #E0E0E0; + white-space: nowrap; + text-align: center; + line-height: 1.2; + margin-top: 4rpx; +} + +/* 操作按钮 */ +.action-buttons { + margin: 40rpx 0rpx 100rpx 0rpx; +} + +.action-btn { + padding: 28rpx; + text-align: center; + border-radius: 16rpx; + font-size: 32rpx; + font-weight: bold; + border: none; +} + +.action-btn.primary { + background: linear-gradient(135deg, #50C878 0%, #06a854 100%); + color: white; + box-shadow: 0 8rpx 20rpx rgba(7, 193, 96, 0.3); + +} +/* 为TabBar预留空间 */ +.container { + padding-bottom: 120rpx; +} diff --git a/pages/stats/session/session.js b/pages/stats/session/session.js new file mode 100644 index 0000000..196efb2 --- /dev/null +++ b/pages/stats/session/session.js @@ -0,0 +1,552 @@ +// pages/stats/session/session.js +import request from '../../../utils/request' + +Page({ + data: { + sessionId: null, + session: null, + finalRanking: [], + playerStats: [], + recentRecords: [], + totalRounds: 0, + totalChips: 0, + duration: '', + + // 图表数据 + chartLegend: [], + chartWidth: 750, + scoreData: [], + + // 分析数据 + mostIntenseRound: 0, + mostIntenseScore: 0, + avgRoundScore: 0, + longestStreak: { player: '', count: 0 }, + comebackKing: { player: '', points: 0 }, + + // 分享海报 + showSharePoster: false, + + gameTypeText: { + 'mahjong': '麻将', + 'poker': '扑克', + 'other': '其他' + } + }, + + onLoad(options) { + if (!options.id) { + wx.showToast({ + title: '参数错误', + icon: 'none' + }) + setTimeout(() => { + wx.navigateBack() + }, 1500) + return + } + + this.setData({ sessionId: options.id }) + this.loadStatistics() + }, + + onReady() { + // 延迟绘制图表,确保canvas已准备好 + setTimeout(() => { + this.drawScoreChart() + }, 500) + }, + + // 加载统计数据 + async loadStatistics() { + wx.showLoading({ title: '加载中...' }) + + try { + // 获取牌局详情 + const sessionData = await request.get(`/rooms/${this.data.sessionId}`) + + // 获取统计数据 + const statsData = await request.get(`/stats/session/${this.data.sessionId}`) + + // 获取所有记录 + const recordsData = await request.get(`/records/session/${this.data.sessionId}`, { + page: 1, + pageSize: 100 + }) + + // 计算游戏时长 + const duration = this.calculateDuration(sessionData.session) + + // 计算最终排名 + const finalRanking = this.calculateFinalRanking(sessionData.players, statsData) + + // 计算玩家统计 + const playerStats = this.calculatePlayerStats(statsData, recordsData.list) + + // 准备图表数据 + const chartData = this.prepareChartData(recordsData.list, sessionData.players) + + // 分析数据 + const analysis = this.analyzeData(recordsData.list, sessionData.players) + + // 格式化最近记录 + const recentRecords = this.formatRecords(recordsData.list.slice(0, 10)) + + this.setData({ + session: sessionData.session, + finalRanking, + playerStats, + recentRecords, + totalRounds: recordsData.total || 0, + totalChips: Math.abs(statsData.total_chips || 0), + duration, + chartLegend: chartData.legend, + scoreData: chartData.data, + chartWidth: Math.max(750, chartData.data.length * 50), + ...analysis + }) + + wx.hideLoading() + + } catch (error) { + wx.hideLoading() + wx.showToast({ + title: error.message || '加载失败', + icon: 'none' + }) + } + }, + + // 计算游戏时长 + calculateDuration(session) { + const start = session.created_at * 1000 + const end = session.ended_at ? session.ended_at * 1000 : Date.now() + const diff = end - start + + const hours = Math.floor(diff / 3600000) + const minutes = Math.floor((diff % 3600000) / 60000) + + if (hours > 0) { + return `${hours}小时${minutes}分钟` + } else { + return `${minutes}分钟` + } + }, + + // 计算最终排名 + calculateFinalRanking(players, stats) { + return players.map(p => { + const playerStats = stats.players?.[p.player_id] || {} + const winRounds = playerStats.win_rounds || 0 + const totalRounds = playerStats.total_rounds || 1 + + return { + player_id: p.player_id, + nickname: p.nickname, + avatar_url: p.avatar_url, + final_chips: p.final_chips || 0, + win_rate: totalRounds > 0 ? Math.round((winRounds / totalRounds) * 100) : 0, + rounds_played: totalRounds + } + }).sort((a, b) => b.final_chips - a.final_chips) + }, + + // 计算玩家统计 + calculatePlayerStats(stats, records) { + const playerStats = [] + + for (const playerId in stats.players) { + const player = stats.players[playerId] + + // 计算每个玩家的详细统计 + const playerRecords = [] + records.forEach(record => { + const score = record.playerScores?.find(s => s.player_id == playerId) + if (score) { + playerRecords.push(score.chips_change) + } + }) + + const maxWin = Math.max(...playerRecords.filter(s => s > 0), 0) + const maxLose = Math.min(...playerRecords.filter(s => s < 0), 0) + const avgScore = playerRecords.length > 0 + ? Math.round(playerRecords.reduce((a, b) => a + b, 0) / playerRecords.length) + : 0 + + // 计算连胜 + let currentStreak = 0 + let maxStreak = 0 + playerRecords.forEach(score => { + if (score > 0) { + currentStreak++ + maxStreak = Math.max(maxStreak, currentStreak) + } else { + currentStreak = 0 + } + }) + + playerStats.push({ + player_id: playerId, + nickname: player.nickname, + avatar_url: player.avatar_url, + max_win: maxWin, + max_lose: maxLose, + avg_score: avgScore, + win_streak: maxStreak + }) + } + + return playerStats + }, + + // 准备图表数据 + prepareChartData(records, players) { + const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD'] + const legend = players.map((p, i) => ({ + player_id: p.player_id, + nickname: p.nickname, + color: colors[i % colors.length] + })) + + // 构建积分走势数据 + const data = [] + const runningScores = {} + + // 初始化积分 + players.forEach(p => { + runningScores[p.player_id] = 0 + }) + + // 添加起始点 + data.push({ + round: 0, + scores: { ...runningScores } + }) + + // 计算每轮后的积分 + records.forEach(record => { + record.playerScores?.forEach(score => { + runningScores[score.player_id] = (runningScores[score.player_id] || 0) + score.chips_change + }) + + data.push({ + round: record.round_number, + scores: { ...runningScores } + }) + }) + + return { legend, data } + }, + + // 分析数据 + analyzeData(records, players) { + // 最激烈对局 + let mostIntenseRound = 0 + let mostIntenseScore = 0 + + records.forEach(record => { + const totalChange = record.playerScores?.reduce((sum, s) => + sum + Math.abs(s.chips_change), 0) || 0 + + if (totalChange > mostIntenseScore) { + mostIntenseScore = totalChange + mostIntenseRound = record.round_number + } + }) + + // 平均每局分数 + const totalScore = records.reduce((sum, record) => { + const roundTotal = record.playerScores?.reduce((s, score) => + s + Math.abs(score.chips_change), 0) || 0 + return sum + roundTotal + }, 0) + const avgRoundScore = records.length > 0 + ? Math.round(totalScore / records.length / 2) + : 0 + + // 最长连胜 + const streaks = {} + const currentStreaks = {} + + players.forEach(p => { + streaks[p.player_id] = 0 + currentStreaks[p.player_id] = 0 + }) + + records.forEach(record => { + record.playerScores?.forEach(score => { + if (score.chips_change > 0) { + currentStreaks[score.player_id] = (currentStreaks[score.player_id] || 0) + 1 + streaks[score.player_id] = Math.max( + streaks[score.player_id] || 0, + currentStreaks[score.player_id] + ) + } else { + currentStreaks[score.player_id] = 0 + } + }) + }) + + let longestStreak = { player: '', count: 0 } + for (const playerId in streaks) { + if (streaks[playerId] > longestStreak.count) { + const player = players.find(p => p.player_id == playerId) + longestStreak = { + player: player?.nickname || '', + count: streaks[playerId] + } + } + } + + // 逆袭王(从最低点到最高点的差值最大的玩家) + const comebacks = {} + const runningScores = {} + const minScores = {} + + players.forEach(p => { + runningScores[p.player_id] = 0 + minScores[p.player_id] = 0 + comebacks[p.player_id] = 0 + }) + + records.forEach(record => { + record.playerScores?.forEach(score => { + runningScores[score.player_id] = (runningScores[score.player_id] || 0) + score.chips_change + minScores[score.player_id] = Math.min( + minScores[score.player_id] || 0, + runningScores[score.player_id] + ) + }) + }) + + let comebackKing = { player: '', points: 0 } + for (const playerId in runningScores) { + const comeback = runningScores[playerId] - minScores[playerId] + if (comeback > comebackKing.points) { + const player = players.find(p => p.player_id == playerId) + comebackKing = { + player: player?.nickname || '', + points: comeback + } + } + } + + return { + mostIntenseRound, + mostIntenseScore: mostIntenseScore / 2, + avgRoundScore, + longestStreak, + comebackKing + } + }, + + // 格式化记录 + formatRecords(records) { + return records.map(record => { + const time = new Date(record.created_at * 1000) + return { + ...record, + timeText: `${time.getMonth() + 1}/${time.getDate()} ${time.getHours()}:${String(time.getMinutes()).padStart(2, '0')}` + } + }) + }, + + // 绘制积分走势图 + drawScoreChart() { + const ctx = wx.createCanvasContext('scoreChart') + const { scoreData, chartLegend } = this.data + + if (scoreData.length === 0) return + + const width = this.data.chartWidth + const height = 300 + const padding = 40 + const chartWidth = width - padding * 2 + const chartHeight = height - padding * 2 + + // 找出最大最小值 + let minScore = 0 + let maxScore = 0 + scoreData.forEach(data => { + for (const playerId in data.scores) { + minScore = Math.min(minScore, data.scores[playerId]) + maxScore = Math.max(maxScore, data.scores[playerId]) + } + }) + + const scoreRange = maxScore - minScore || 100 + const xStep = chartWidth / (scoreData.length - 1 || 1) + const yScale = chartHeight / scoreRange + + // 绘制网格 + ctx.setStrokeStyle('#e0e0e0') + ctx.setLineWidth(0.5) + + // 横线 + for (let i = 0; i <= 5; i++) { + const y = padding + (chartHeight / 5) * i + ctx.beginPath() + ctx.moveTo(padding, y) + ctx.lineTo(width - padding, y) + ctx.stroke() + } + + // 绘制每个玩家的线 + chartLegend.forEach(legend => { + ctx.setStrokeStyle(legend.color) + ctx.setLineWidth(2) + ctx.beginPath() + + scoreData.forEach((data, index) => { + const x = padding + xStep * index + const y = padding + chartHeight - (data.scores[legend.player_id] - minScore) * yScale + + if (index === 0) { + ctx.moveTo(x, y) + } else { + ctx.lineTo(x, y) + } + }) + + ctx.stroke() + + // 绘制点 + ctx.setFillStyle(legend.color) + scoreData.forEach((data, index) => { + const x = padding + xStep * index + const y = padding + chartHeight - (data.scores[legend.player_id] - minScore) * yScale + + ctx.beginPath() + ctx.arc(x, y, 3, 0, 2 * Math.PI) + ctx.fill() + }) + }) + + // 绘制坐标轴标签 + ctx.setFillStyle('#666') + ctx.setFontSize(10) + + // X轴标签 + scoreData.forEach((data, index) => { + if (index % Math.ceil(scoreData.length / 10) === 0) { + const x = padding + xStep * index + ctx.fillText(`第${data.round}局`, x - 15, height - 10) + } + }) + + ctx.draw() + }, + + // 查看所有记录 + viewAllRecords() { + wx.navigateTo({ + url: `/pages/game/records/records?id=${this.data.sessionId}` + }) + }, + + // 导出数据 + async exportData() { + wx.showToast({ + title: '功能开发中', + icon: 'none' + }) + }, + + // 分享战绩 + shareStats() { + this.setData({ showSharePoster: true }) + this.drawSharePoster() + }, + + // 绘制分享海报 + drawSharePoster() { + const ctx = wx.createCanvasContext('sharePoster') + const { session, finalRanking, totalRounds } = this.data + + // 背景 + ctx.setFillStyle('#fff') + ctx.fillRect(0, 0, 375, 600) + + // 标题 + ctx.setFillStyle('#333') + ctx.setFontSize(24) + ctx.setTextAlign('center') + ctx.fillText(session.session_name, 187, 50) + + // 副标题 + ctx.setFontSize(14) + ctx.setFillStyle('#999') + ctx.fillText(`${this.data.gameTypeText[session.game_type]} · 共${totalRounds}局`, 187, 80) + + // 排名 + ctx.setTextAlign('left') + finalRanking.slice(0, 3).forEach((player, index) => { + const y = 150 + index * 80 + + // 排名标记 + const medals = ['🥇', '🥈', '🥉'] + ctx.setFontSize(30) + ctx.fillText(medals[index], 30, y) + + // 玩家名称 + ctx.setFontSize(18) + ctx.setFillStyle('#333') + ctx.fillText(player.nickname, 80, y) + + // 积分 + ctx.setTextAlign('right') + ctx.setFillStyle(player.final_chips > 0 ? '#4CAF50' : '#F44336') + ctx.fillText(`${player.final_chips > 0 ? '+' : ''}${player.final_chips}`, 330, y) + ctx.setTextAlign('left') + }) + + // 底部信息 + ctx.setFillStyle('#999') + ctx.setFontSize(12) + ctx.setTextAlign('center') + ctx.fillText('打牌记账小程序', 187, 550) + + ctx.draw() + }, + + // 保存海报 + savePoster() { + wx.canvasToTempFilePath({ + canvasId: 'sharePoster', + success: (res) => { + wx.saveImageToPhotosAlbum({ + filePath: res.tempFilePath, + success: () => { + wx.showToast({ + title: '已保存到相册', + icon: 'success' + }) + }, + fail: () => { + wx.showToast({ + title: '保存失败', + icon: 'none' + }) + } + }) + } + }) + }, + + // 关闭分享海报 + closeSharePoster() { + this.setData({ showSharePoster: false }) + }, + + // 分享给朋友 + onShareAppMessage() { + const { session, finalRanking } = this.data + const winner = finalRanking[0] + + return { + title: `${winner.nickname}获得${session.session_name}冠军!`, + path: `/pages/stats/session/session?id=${this.data.sessionId}`, + imageUrl: '/images/share-bg.png' + } + } +}) \ No newline at end of file diff --git a/pages/stats/session/session.json b/pages/stats/session/session.json new file mode 100644 index 0000000..8835af0 --- /dev/null +++ b/pages/stats/session/session.json @@ -0,0 +1,3 @@ +{ + "usingComponents": {} +} \ No newline at end of file diff --git a/pages/stats/session/session.wxml b/pages/stats/session/session.wxml new file mode 100644 index 0000000..2cfa7a6 --- /dev/null +++ b/pages/stats/session/session.wxml @@ -0,0 +1,204 @@ + + + + + + {{session.session_name}} + + {{session.status === 'finished' ? '已结束' : '进行中'}} + + + + + + 游戏类型 + {{gameTypeText[session.game_type]}} + + + 基础分值 + {{session.base_score}}分 + + + 总局数 + {{totalRounds}}局 + + + 游戏时长 + {{duration}} + + + + + + + + 最终排名 + 总分池: {{totalChips}}分 + + + + + + {{index + 1}} + 冠军 + 亚军 + 季军 + + + + + {{item.nickname}} + + + + + + {{item.final_chips > 0 ? '+' : ''}}{{item.final_chips}} + + 总分 + + + + {{item.win_rate}}% + 胜率 + + + + {{item.rounds_played}} + 参与局数 + + + + + + + + + 战绩统计 + + + + + + {{item.nickname}} + + + + + 最高单局 + +{{item.max_win}} + + + 最低单局 + {{item.max_lose}} + + + 平均得分 + {{item.avg_score}} + + + 连胜纪录 + {{item.win_streak}}局 + + + + + + + + + + 积分走势 + + + + {{item.nickname}} + + + + + + + + + + + + + 对战记录 + 查看全部 + + + + + + {{item.round_number}} + + + + {{item.timeText}} + + + {{score.nickname}} + + {{score.chips_change > 0 ? '+' : ''}}{{score.chips_change}} + + + + + + + + + + + 数据分析 + + + + 最激烈对局 + 第{{mostIntenseRound}}局 ({{mostIntenseScore}}分) + + + + 平均每局分数 + {{avgRoundScore}}分 + + + + 最长连胜 + {{longestStreak.player}} ({{longestStreak.count}}局) + + + + 逆袭王 + {{comebackKing.player}} ({{comebackKing.points}}分逆转) + + + + + + + + + + + + + \ No newline at end of file diff --git a/pages/stats/session/session.wxss b/pages/stats/session/session.wxss new file mode 100644 index 0000000..fe80459 --- /dev/null +++ b/pages/stats/session/session.wxss @@ -0,0 +1,329 @@ +/* 牌局统计页面样式 */ +.container { + padding-bottom: 100rpx; +} + +/* 牌局信息卡片 */ +.session-info { + margin-bottom: 20rpx; +} + +.session-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 24rpx; +} + +.session-name { + font-size: 36rpx; + font-weight: bold; + color: #FFFFFF; +} + +.session-meta { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20rpx; +} + +.meta-item { + display: flex; + flex-direction: column; + gap: 8rpx; +} + +.meta-label { + font-size: 24rpx; + color: #B0B0B0; +} + +.meta-value { + font-size: 32rpx; + font-weight: bold; + color: #FFFFFF; +} + +/* 最终排名 */ +.final-ranking { + margin-bottom: 20rpx; +} + +.total-chips { + font-size: 24rpx; + color: #B0B0B0; +} + +.ranking-list { + display: flex; + flex-direction: column; + gap: 16rpx; + margin-top: 20rpx; +} + +.ranking-item { + display: flex; + align-items: center; + padding: 20rpx; + background: #f8f9fa; + border-radius: 16rpx; + gap: 16rpx; +} + +.ranking-item.champion { + background: linear-gradient(135deg, #fff9e6 0%, #fff3cc 100%); + border: 2rpx solid #ffd700; +} + +.ranking-item.runner-up { + background: linear-gradient(135deg, #f5f5f5 0%, #e8e8e8 100%); + border: 2rpx solid #c0c0c0; +} + +.ranking-item.third { + background: linear-gradient(135deg, #fff0e6 0%, #ffe6cc 100%); + border: 2rpx solid #cd7f32; +} + +.rank-badge { + display: flex; + flex-direction: column; + align-items: center; + min-width: 60rpx; +} + +.rank-number { + font-size: 40rpx; + font-weight: bold; + color: #FFFFFF; + line-height: 1; +} + +.rank-label { + font-size: 20rpx; + color: #B0B0B0; + margin-top: 4rpx; +} + +.ranking-item.champion .rank-number { + color: #ff9500; +} + +.ranking-item.runner-up .rank-number { + color: #E0E0E0; +} + +.ranking-item.third .rank-number { + color: #cd7f32; +} + +.player-info { + display: flex; + align-items: center; + flex: 1; + gap: 12rpx; +} + +.player-avatar { + width: 64rpx; + height: 64rpx; + border-radius: 50%; + border: 2rpx solid #fff; +} + +.player-name { + font-size: 30rpx; + font-weight: 500; + color: #FFFFFF; +} + +.player-stats { + display: flex; + gap: 24rpx; +} + +.stat-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 4rpx; +} + +.stat-value { + font-size: 28rpx; + font-weight: bold; + color: #FFFFFF; +} + +.stat-label { + font-size: 20rpx; + color: #B0B0B0; +} + +/* 战绩统计 */ +.stats-summary { + margin-bottom: 20rpx; +} + +.stats-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16rpx; + margin-top: 20rpx; +} + +.stats-item { + padding: 20rpx; + background: #f8f9fa; + border-radius: 12rpx; + text-align: center; +} + +.stats-number { + font-size: 40rpx; + font-weight: bold; + color: #FFFFFF; + display: block; + margin-bottom: 8rpx; +} + +.stats-label { + font-size: 24rpx; + color: #B0B0B0; +} + +/* 图表容器 */ +.chart-card { + margin-bottom: 20rpx; +} + +.chart-container { + width: 100%; + height: 400rpx; + margin-top: 20rpx; +} + +/* 趋势分析 */ +.trend-analysis { + margin-bottom: 20rpx; +} + +.trend-list { + display: flex; + flex-direction: column; + gap: 16rpx; + margin-top: 20rpx; +} + +.trend-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20rpx; + background: #f8f9fa; + border-radius: 12rpx; +} + +.trend-info { + flex: 1; +} + +.trend-title { + font-size: 28rpx; + color: #FFFFFF; + margin-bottom: 8rpx; + display: block; +} + +.trend-desc { + font-size: 24rpx; + color: #B0B0B0; +} + +.trend-value { + font-size: 36rpx; + font-weight: bold; +} + +/* 底部操作按钮 */ +.bottom-actions { + display: flex; + gap: 16rpx; + padding: 0 20rpx 20rpx; +} + +.bottom-actions button { + flex: 1; +} + +/* 结算详情弹窗 */ +.settlement-modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 1000; +} + +.settlement-list { + display: flex; + flex-direction: column; + gap: 16rpx; +} + +.settlement-item { + padding: 24rpx; + background: #f8f9fa; + border-radius: 12rpx; +} + +.settlement-flow { + display: flex; + align-items: center; + gap: 16rpx; + margin-bottom: 12rpx; +} + +.settlement-player { + flex: 1; + display: flex; + align-items: center; + gap: 12rpx; +} + +.settlement-arrow { + font-size: 32rpx; + color: #50C878; +} + +.settlement-amount { + font-size: 36rpx; + font-weight: bold; + color: #50C878; + text-align: center; +} + +.settlement-method { + font-size: 24rpx; + color: #B0B0B0; + text-align: center; + margin-top: 8rpx; +} + +.settlement-status { + padding: 4rpx 12rpx; + border-radius: 12rpx; + font-size: 24rpx; + margin-left: 8rpx; +} + +.settlement-status.paid { + background: #e8f5e9; + color: #50C878; +} + +.settlement-status.pending { + background: #1A1A1A3e0; + color: #ff9500; +} \ No newline at end of file diff --git a/project.config.json b/project.config.json new file mode 100644 index 0000000..960d67c --- /dev/null +++ b/project.config.json @@ -0,0 +1,41 @@ +{ + "compileType": "miniprogram", + "libVersion": "3.10.1", + "packOptions": { + "ignore": [], + "include": [] + }, + "setting": { + "coverView": true, + "es6": true, + "postcss": true, + "minified": true, + "enhance": true, + "showShadowRootInWxmlPanel": true, + "packNpmRelationList": [], + "babelSetting": { + "ignore": [], + "disablePlugins": [], + "outputPath": "" + }, + "compileWorklet": false, + "uglifyFileName": false, + "uploadWithSourceMap": true, + "packNpmManually": false, + "minifyWXSS": true, + "minifyWXML": true, + "localPlugins": false, + "condition": false, + "swc": false, + "disableSWC": true, + "disableUseStrict": false, + "useCompilerPlugins": false + }, + "condition": {}, + "editorSetting": { + "tabIndent": "auto", + "tabSize": 2 + }, + "appid": "wxe4ef1cc6e75de032", + "simulatorPluginLibVersion": {} +} \ No newline at end of file diff --git a/project.private.config.json b/project.private.config.json new file mode 100644 index 0000000..30e0f47 --- /dev/null +++ b/project.private.config.json @@ -0,0 +1,24 @@ +{ + "description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html", + "projectname": "miniapp3", + "setting": { + "compileHotReLoad": true, + "urlCheck": false, + "coverView": true, + "lazyloadPlaceholderEnable": false, + "skylineRenderEnable": false, + "preloadBackgroundData": false, + "autoAudits": false, + "useApiHook": true, + "useApiHostProcess": true, + "showShadowRootInWxmlPanel": true, + "useStaticServer": false, + "useLanDebug": false, + "showES6CompileOption": false, + "bigPackageSizeSupport": false, + "checkInvalidKey": true, + "ignoreDevUnusedFiles": true + }, + "libVersion": "3.10.1", + "condition": {} +} \ No newline at end of file diff --git a/sitemap.json b/sitemap.json new file mode 100644 index 0000000..ca02add --- /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/styles/common.wxss b/styles/common.wxss new file mode 100644 index 0000000..91995d3 --- /dev/null +++ b/styles/common.wxss @@ -0,0 +1,235 @@ +/* 全局通用样式 - 打牌记账 */ + +/* ========== 颜色变量 ========== */ +/* 主色调:紫色渐变 */ +/* 辅助色:绿色(成功)、橙色(警告)、红色(错误) */ + +/* ========== 基础样式 ========== */ + +/* 白色卡片 - 统一风格 */ +.card { + background: #1A1A1A; + border-radius: 20rpx; + padding: 32rpx 24rpx; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.15); + margin-bottom: 24rpx; +} + +/* 卡片标题 */ +.card-title { + font-size: 32rpx; + font-weight: bold; + color: #FFFFFF; + margin-bottom: 20rpx; +} + +/* 卡片副标题 */ +.card-subtitle { + font-size: 26rpx; + color: #B0B0B0; + margin-bottom: 16rpx; +} + +/* ========== 表单组件 ========== */ + +/* 表单标签 */ +.form-label { + display: block; + font-size: 28rpx; + font-weight: 600; + color: #FFFFFF; + margin-bottom: 16rpx; +} + +/* 输入框 */ +.form-input { + width: 100%; + background: #f8f9fa; + border: 2rpx solid rgba(0, 0, 0, 0.1); + border-radius: 12rpx; + padding: 24rpx 20rpx; + font-size: 30rpx; + color: #FFFFFF; + box-sizing: border-box; + transition: all 0.3s; +} + +.form-input:focus { + border-color: #1A1A1A; + background: #1A1A1A; + box-shadow: 0 0 0 4rpx rgba(0, 0, 0, 0.1); +} + +.form-input::placeholder { + color: #ccc; +} + +/* 提示文字 */ +.form-hint { + font-size: 24rpx; + color: #B0B0B0; + margin-top: 12rpx; + display: block; +} + +/* ========== 按钮样式 ========== */ + +/* 主按钮 - 绿色渐变 */ +.btn-primary { + width: 100%; + background: linear-gradient(135deg, #50C878 0%, #06a854 100%); + color: white; + border-radius: 16rpx; + padding: 28rpx; + font-size: 32rpx; + font-weight: bold; + border: none; + box-shadow: 0 8rpx 24rpx rgba(7, 193, 96, 0.4); + transition: all 0.3s; +} + +.btn-primary:active { + opacity: 0.9; + transform: scale(0.98); +} + +.btn-primary[disabled] { + background: #e0e0e0; + color: #B0B0B0; + box-shadow: none; +} + +/* 次要按钮 - 橙色渐变 */ +.btn-secondary { + width: 100%; + background: linear-gradient(135deg, #ff9a76 0%, #ff7e5f 100%); + color: white; + border-radius: 16rpx; + padding: 28rpx; + font-size: 32rpx; + font-weight: bold; + border: none; + box-shadow: 0 8rpx 24rpx rgba(255, 154, 118, 0.4); +} + +.btn-secondary:active { + opacity: 0.9; + transform: scale(0.98); +} + +/* 紫色按钮 */ +.btn-purple { + width: 100%; + background: linear-gradient(135deg, #1A1A1A 0%, #252525 100%); + color: white; + border-radius: 16rpx; + padding: 28rpx; + font-size: 32rpx; + font-weight: bold; + border: none; + box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.4); +} + +.btn-purple:active { + opacity: 0.9; + transform: scale(0.98); +} + +/* 轮廓按钮 */ +.btn-outline { + width: 100%; + background: #1A1A1A; + color: #1A1A1A; + border: 2rpx solid #1A1A1A; + border-radius: 16rpx; + padding: 28rpx; + font-size: 32rpx; + font-weight: bold; + box-shadow: none; +} + +.btn-outline:active { + background: rgba(0, 0, 0, 0.05); +} + +/* ========== 文字样式 ========== */ + +.text-primary { + color: #1A1A1A; +} + +.text-success { + color: #50C878; +} + +.text-warning { + color: #ff9500; +} + +.text-error { + color: #ff3b30; +} + +.text-muted { + color: #B0B0B0; +} + +.text-white { + color: white; +} + +/* ========== 标签样式 ========== */ + +.tag { + display: inline-block; + padding: 8rpx 20rpx; + border-radius: 8rpx; + font-size: 24rpx; + font-weight: 500; +} + +.tag-success { + background: rgba(7, 193, 96, 0.1); + color: #50C878; +} + +.tag-warning { + background: rgba(255, 149, 0, 0.1); + color: #ff9500; +} + +.tag-error { + background: rgba(255, 59, 48, 0.1); + color: #ff3b30; +} + +.tag-info { + background: rgba(0, 0, 0, 0.1); + color: #1A1A1A; +} + +/* ========== 分隔线 ========== */ + +.divider { + height: 1rpx; + background: rgba(0, 0, 0, 0.05); + margin: 24rpx 0; +} + +/* ========== 空状态 ========== */ + +.empty-state { + text-align: center; + padding: 100rpx 0; +} + +.empty-icon { + font-size: 80rpx; + opacity: 0.3; + margin-bottom: 20rpx; +} + +.empty-text { + font-size: 28rpx; + color: rgba(255, 255, 255, 0.6); +} diff --git a/styles/theme.wxss b/styles/theme.wxss new file mode 100644 index 0000000..09e475b --- /dev/null +++ b/styles/theme.wxss @@ -0,0 +1,390 @@ +/** + * 黑金主题配置 - 高端奢华风格 + * 风格:奢华、高端、精致 + */ + +/* ==================== 颜色变量 ==================== */ + +:root { + /* 主品牌色 - 金色 */ + --primary-gold: #D4AF37; + --primary-gold-light: #F4E6C3; + --primary-gold-dark: #B8941F; + --primary-gold-deep: #9A7B1A; + + /* 辅助色 - 银色 */ + --secondary-silver: #C0C0C0; + --secondary-silver-light: #E5E5E5; + --accent-platinum: #E5E4E2; + + /* 背景色系 - 黑色系 */ + --bg-black: #000000; + --bg-dark: #0A0A0A; + --bg-darker: #121212; + --bg-card: #1A1A1A; + --bg-card-hover: #222222; + --bg-secondary: #252525; + + /* 文字色系 */ + --text-primary: #FFFFFF; + --text-secondary: #E0E0E0; + --text-tertiary: #B0B0B0; + --text-disabled: #606060; + --text-gold: #D4AF37; + + /* 状态色 */ + --success: #50C878; + --warning: #FFD700; + --error: #DC143C; + --info: #4682B4; + + /* 边框色 */ + --border-dark: #2A2A2A; + --border-medium: #3A3A3A; + --border-light: #4A4A4A; + --border-gold: rgba(212, 175, 55, 0.3); + + /* 阴影 */ + --shadow-sm: 0 2rpx 8rpx rgba(0, 0, 0, 0.6); + --shadow-md: 0 4rpx 16rpx rgba(0, 0, 0, 0.7); + --shadow-lg: 0 8rpx 32rpx rgba(0, 0, 0, 0.8); + --shadow-gold: 0 4rpx 20rpx rgba(212, 175, 55, 0.4); + --shadow-gold-lg: 0 8rpx 32rpx rgba(212, 175, 55, 0.5); + + /* 发光效果 */ + --glow-gold: 0 0 20rpx rgba(212, 175, 55, 0.6), + 0 0 40rpx rgba(212, 175, 55, 0.4), + 0 0 60rpx rgba(212, 175, 55, 0.2); + + /* 圆角 */ + --radius-sm: 8rpx; + --radius-md: 12rpx; + --radius-lg: 16rpx; + --radius-xl: 20rpx; + --radius-full: 9999rpx; + + /* 间距 */ + --space-xs: 8rpx; + --space-sm: 12rpx; + --space-md: 16rpx; + --space-lg: 24rpx; + --space-xl: 32rpx; +} + +/* ==================== 渐变定义 ==================== */ + +.gradient-gold { + background: linear-gradient(135deg, #D4AF37 0%, #F4E6C3 50%, #D4AF37 100%); +} + +.gradient-gold-vertical { + background: linear-gradient(180deg, #D4AF37 0%, #B8941F 100%); +} + +.gradient-dark { + background: linear-gradient(180deg, #000000 0%, #1A1A1A 100%); +} + +.gradient-dark-card { + background: linear-gradient(135deg, #1A1A1A 0%, #252525 100%); +} + +.gradient-silver { + background: linear-gradient(135deg, #C0C0C0 0%, #E5E5E5 100%); +} + +/* 金属质感渐变 */ +.gradient-metallic-gold { + background: linear-gradient(135deg, + #B8941F 0%, + #D4AF37 25%, + #F4E6C3 50%, + #D4AF37 75%, + #B8941F 100% + ); +} + +/* ==================== 文字颜色类 ==================== */ + +.text-primary { color: var(--text-primary); } +.text-secondary { color: var(--text-secondary); } +.text-tertiary { color: var(--text-tertiary); } +.text-disabled { color: var(--text-disabled); } +.text-gold { color: var(--primary-gold); } + +/* 金色渐变文字 */ +.text-gradient-gold { + background: linear-gradient(135deg, #D4AF37 0%, #F4E6C3 50%, #D4AF37 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.text-success { color: var(--success); } +.text-warning { color: var(--warning); } +.text-error { color: var(--error); } + +/* ==================== 背景颜色类 ==================== */ + +.bg-black { background-color: var(--bg-black); } +.bg-dark { background-color: var(--bg-dark); } +.bg-darker { background-color: var(--bg-darker); } +.bg-card { background-color: var(--bg-card); } +.bg-secondary { background-color: var(--bg-secondary); } + +/* ==================== 阴影类 ==================== */ + +.shadow-sm { box-shadow: var(--shadow-sm); } +.shadow-md { box-shadow: var(--shadow-md); } +.shadow-lg { box-shadow: var(--shadow-lg); } +.shadow-gold { box-shadow: var(--shadow-gold); } +.shadow-gold-lg { box-shadow: var(--shadow-gold-lg); } + +/* ==================== 边框类 ==================== */ + +.border-dark { border: 1rpx solid var(--border-dark); } +.border-medium { border: 1rpx solid var(--border-medium); } +.border-light { border: 1rpx solid var(--border-light); } +.border-gold { border: 1rpx solid var(--primary-gold); } + +/* 金色渐变边框 */ +.border-gradient-gold { + position: relative; + background: var(--bg-card); + border-radius: var(--radius-lg); +} + +.border-gradient-gold::before { + content: ''; + position: absolute; + inset: 0; + padding: 2rpx; + border-radius: var(--radius-lg); + background: linear-gradient(135deg, #D4AF37 0%, #F4E6C3 50%, #D4AF37 100%); + -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); + -webkit-mask-composite: xor; + mask-composite: exclude; +} + +/* ==================== 按钮样式 ==================== */ + +.btn-luxury { + border-radius: var(--radius-md); + font-weight: 600; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + overflow: hidden; +} + +.btn-luxury::before { + content: ''; + position: absolute; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: radial-gradient(circle, rgba(255, 255, 255, 0.2) 0%, transparent 70%); + opacity: 0; + transition: opacity 0.3s ease; +} + +.btn-luxury:active::before { + opacity: 1; +} + +/* ==================== 卡片样式 ==================== */ + +.card-luxury { + background: var(--bg-card); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-md); + border: 1rpx solid var(--border-dark); + overflow: hidden; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.card-luxury:active { + background: var(--bg-card-hover); + box-shadow: var(--shadow-lg); + transform: translateY(-2rpx); +} + +/* 金色装饰卡片 */ +.card-gold-accent { + position: relative; +} + +.card-gold-accent::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3rpx; + background: linear-gradient(90deg, transparent 0%, #D4AF37 50%, transparent 100%); +} + +/* ==================== 徽章样式 ==================== */ + +.badge-luxury { + display: inline-flex; + align-items: center; + padding: 6rpx 16rpx; + border-radius: var(--radius-full); + font-size: 20rpx; + font-weight: 600; + border: 1rpx solid; +} + +.badge-gold { + background: linear-gradient(135deg, rgba(212, 175, 55, 0.15) 0%, rgba(244, 230, 195, 0.2) 100%); + color: var(--primary-gold); + border-color: var(--primary-gold); +} + +.badge-silver { + background: rgba(192, 192, 192, 0.15); + color: var(--secondary-silver); + border-color: var(--secondary-silver); +} + +/* ==================== 特殊效果 ==================== */ + +/* 金属质感 */ +.metallic-gold { + background: linear-gradient(135deg, + #9A7B1A 0%, + #B8941F 20%, + #D4AF37 40%, + #F4E6C3 50%, + #D4AF37 60%, + #B8941F 80%, + #9A7B1A 100% + ); + background-size: 200% 200%; + animation: shimmer 3s linear infinite; +} + +@keyframes shimmer { + 0% { background-position: 0% 0%; } + 50% { background-position: 100% 100%; } + 100% { background-position: 0% 0%; } +} + +/* 发光效果 */ +.glow-gold { + box-shadow: var(--glow-gold); +} + +.glow-gold-hover:active { + box-shadow: var(--glow-gold); +} + +/* 玻璃态效果 - 深色版 */ +.glass-dark { + background: rgba(26, 26, 26, 0.8); + backdrop-filter: blur(20rpx) saturate(180%); + -webkit-backdrop-filter: blur(20rpx) saturate(180%); + border: 1rpx solid rgba(255, 255, 255, 0.1); +} + +/* 划线装饰 */ +.divider-gold { + height: 2rpx; + background: linear-gradient(90deg, + transparent 0%, + var(--primary-gold) 20%, + var(--primary-gold) 80%, + transparent 100% + ); +} + +/* ==================== 动画效果 ==================== */ + +@keyframes fadeInDark { + from { + opacity: 0; + transform: translateY(20rpx); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes slideInGold { + from { + opacity: 0; + transform: translateX(-40rpx); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes glowPulse { + 0%, 100% { + box-shadow: 0 0 20rpx rgba(212, 175, 55, 0.4); + } + 50% { + box-shadow: 0 0 40rpx rgba(212, 175, 55, 0.8); + } +} + +.animate-fade-in { animation: fadeInDark 0.4s ease-out; } +.animate-slide-in { animation: slideInGold 0.5s ease-out; } +.animate-glow { animation: glowPulse 2s ease-in-out infinite; } + +/* ==================== 响应式字体 ==================== */ + +.text-xs { font-size: 20rpx; line-height: 1.4; } +.text-sm { font-size: 24rpx; line-height: 1.5; } +.text-base { font-size: 28rpx; line-height: 1.5; } +.text-lg { font-size: 32rpx; line-height: 1.4; } +.text-xl { font-size: 36rpx; line-height: 1.3; } +.text-2xl { font-size: 40rpx; line-height: 1.2; } +.text-3xl { font-size: 48rpx; line-height: 1.1; } + +.font-light { font-weight: 300; } +.font-normal { font-weight: 400; } +.font-medium { font-weight: 500; } +.font-semibold { font-weight: 600; } +.font-bold { font-weight: 700; } + +/* ==================== 实用工具类 ==================== */ + +.rounded-sm { border-radius: var(--radius-sm); } +.rounded-md { border-radius: var(--radius-md); } +.rounded-lg { border-radius: var(--radius-lg); } +.rounded-xl { border-radius: var(--radius-xl); } +.rounded-full { border-radius: var(--radius-full); } + +.overflow-hidden { overflow: hidden; } +.text-center { text-align: center; } +.text-left { text-align: left; } +.text-right { text-align: right; } + +.flex { display: flex; } +.flex-col { flex-direction: column; } +.items-center { align-items: center; } +.items-start { align-items: flex-start; } +.justify-center { justify-content: center; } +.justify-between { justify-content: space-between; } +.flex-1 { flex: 1; } + +.gap-xs { gap: var(--space-xs); } +.gap-sm { gap: var(--space-sm); } +.gap-md { gap: var(--space-md); } +.gap-lg { gap: var(--space-lg); } + +.w-full { width: 100%; } +.h-full { height: 100%; } + +.relative { position: relative; } +.absolute { position: absolute; } +.fixed { position: fixed; } + +.z-10 { z-index: 10; } +.z-50 { z-index: 50; } +.z-100 { z-index: 100; } +.z-999 { z-index: 999; } diff --git a/utils/format.js b/utils/format.js new file mode 100644 index 0000000..07c88bb --- /dev/null +++ b/utils/format.js @@ -0,0 +1,116 @@ +// 格式化工具函数 + +/** + * 格式化时间戳 + * @param {number} timestamp - UNIX时间戳 + * @param {string} format - 格式化模板 + */ +export function formatTime(timestamp, format = 'YYYY-MM-DD HH:mm:ss') { + const date = new Date(timestamp * 1000) + + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + const hours = String(date.getHours()).padStart(2, '0') + const minutes = String(date.getMinutes()).padStart(2, '0') + const seconds = String(date.getSeconds()).padStart(2, '0') + + return format + .replace('YYYY', year) + .replace('MM', month) + .replace('DD', day) + .replace('HH', hours) + .replace('mm', minutes) + .replace('ss', seconds) +} + +/** + * 格式化金额(分转元) + * @param {number} amount - 金额(分) + */ +export function formatMoney(amount) { + return (amount / 100).toFixed(2) +} + +/** + * 格式化输赢金额 + * @param {number} amount - 金额(分) + */ +export function formatWinLoss(amount) { + const yuan = formatMoney(Math.abs(amount)) + if (amount > 0) { + return `+${yuan}` + } else if (amount < 0) { + return `-${yuan}` + } else { + return '0.00' + } +} + +/** + * 获取相对时间 + * @param {number} timestamp - UNIX时间戳 + */ +export function getRelativeTime(timestamp) { + const now = Math.floor(Date.now() / 1000) + const diff = now - timestamp + + if (diff < 60) { + return '刚刚' + } else if (diff < 3600) { + return `${Math.floor(diff / 60)}分钟前` + } else if (diff < 86400) { + return `${Math.floor(diff / 3600)}小时前` + } else if (diff < 604800) { + return `${Math.floor(diff / 86400)}天前` + } else { + return formatTime(timestamp, 'MM-DD') + } +} + +/** + * 获取游戏类型名称 + * @param {string} type - 游戏类型 + */ +export function getGameTypeName(type) { + const types = { + 'mahjong': '麻将', + 'poker': '扑克', + 'other': '其他' + } + return types[type] || type +} + +/** + * 获取牌局状态名称 + * @param {string} status - 牌局状态 + */ +export function getSessionStatusName(status) { + const statusMap = { + 'waiting': '等待中', + 'playing': '游戏中', + 'paused': '已暂停', + 'finished': '已结束', + 'cancelled': '已取消' + } + return statusMap[status] || status +} + +/** + * 获取牌局状态颜色 + * @param {string} status - 牌局状态 + */ +export function getSessionStatusColor(status) { + const colorMap = { + 'waiting': '#ff9500', + 'playing': '#07c160', + 'paused': '#999999', + 'finished': '#333333', + 'cancelled': '#ff3b30' + } + return colorMap[status] || '#666666' +} + +export function onMyLoginReady(callback) { + +} \ No newline at end of file diff --git a/utils/request.js b/utils/request.js new file mode 100644 index 0000000..3ece035 --- /dev/null +++ b/utils/request.js @@ -0,0 +1,81 @@ +import { gatewayHttpClient } from '@jdmini/api' + +// API路径 +const API_PATH = '/mp/jd-dapaijizhang' + +// 请求封装 +class Request { + // 通用请求方法 + // 网关请求。参数:路径、方法、数据、其他选项(如headers、responseType) + async request(url, method = 'GET', data = {}, options = {}) { + try { + // 构建完整路径 + const fullPath = `${API_PATH}${url}` + + // 合并选项 + const requestOptions = { + headers: { + 'Content-Type': 'application/json', + ...options.headers + }, + ...options + } + + // 使用网关请求,参数:路径、方法、数据、其他选项 + const res = await gatewayHttpClient.request(fullPath, method, data, requestOptions) + + // 处理响应 + if (res && res.code === 200) { + // 成功,返回data字段 + return res.data + } else if (res && res.code === 401) { + // 未授权,跳转登录 + wx.showToast({ + title: res.message || '请重新登录', + icon: 'none' + }) + setTimeout(() => { + wx.navigateTo({ + url: '/pages/login/login' + }) + }, 1500) + throw res + } else { + // 其他错误,不自动显示toast,让调用者处理 + throw res || { code: 500, message: '请求失败' } + } + } catch (error) { + console.error('请求错误:', error) + throw error + } + } + + // GET请求 + get(url, params = {}, options = {}) { + // 将params转换为查询字符串 + const queryString = Object.keys(params) + .map(key => `${key}=${encodeURIComponent(params[key])}`) + .join('&') + + const finalUrl = queryString ? `${url}?${queryString}` : url + return this.request(finalUrl, 'GET', {}, options) + } + + // POST请求 + post(url, data = {}, options = {}) { + return this.request(url, 'POST', data, options) + } + + // PUT请求 + put(url, data = {}, options = {}) { + return this.request(url, 'PUT', data, options) + } + + // DELETE请求 + delete(url, options = {}) { + return this.request(url, 'DELETE', {}, options) + } +} + +// 导出实例 +export default new Request() \ No newline at end of file