Browse Source

添加抽奖 大牌点餐

wenjie 7 months ago
parent
commit
bba6c51043
59 changed files with 6102 additions and 195 deletions
  1. 11 1
      api/goods.js
  2. 76 0
      api/luckyDraw.js
  3. BIN
      components/.DS_Store
  4. BIN
      components/@lucky-canvas/.DS_Store
  5. 201 0
      components/@lucky-canvas/uni/LICENSE
  6. 138 0
      components/@lucky-canvas/uni/README.md
  7. 317 0
      components/@lucky-canvas/uni/lucky-grid.vue
  8. 255 0
      components/@lucky-canvas/uni/lucky-wheel.vue
  9. 23 0
      components/@lucky-canvas/uni/package.json
  10. 214 0
      components/@lucky-canvas/uni/slot-machine.vue
  11. 82 0
      components/@lucky-canvas/uni/utils.js
  12. 201 0
      components/lucky-canvas/LICENSE
  13. 42 0
      components/lucky-canvas/README.md
  14. 15 0
      components/lucky-canvas/dist/index.cjs.js
  15. 0 0
      components/lucky-canvas/dist/index.cjs.js.map
  16. 14 0
      components/lucky-canvas/dist/index.esm.js
  17. 0 0
      components/lucky-canvas/dist/index.esm.js.map
  18. 14 0
      components/lucky-canvas/dist/index.umd.js
  19. 0 0
      components/lucky-canvas/dist/index.umd.js.map
  20. 14 0
      components/lucky-canvas/dist/lucky-canvas.js
  21. 1 0
      components/lucky-canvas/index.js
  22. 66 0
      components/lucky-canvas/package.json
  23. 752 0
      components/lucky-canvas/types/index.d.ts
  24. 143 0
      components/zs-luck-dialog/index.vue
  25. 3 0
      detail/discountsDetail/index.vue
  26. 3 2
      detail/shopDetail/shopDetail.vue
  27. 88 11
      detail/virtualGoods/detail.vue
  28. 43 31
      detail/virtualGoods/index.vue
  29. 7 15
      detail/virtualGoods/pay.vue
  30. 921 0
      luckyDraw/index.vue
  31. 412 0
      luckyDraw/prizeList.vue
  32. 3 3
      movie/index.vue
  33. 8 1
      my/coupon/index.vue
  34. 16 3
      my/memberCenter/index.vue
  35. 1 1
      my/memberCenter/logList.vue
  36. 7 33
      my/order/detail.vue
  37. 3 2
      my/order/hotel/detail.vue
  38. 3 1
      my/order/index.vue
  39. 4 8
      my/order/refund.vue
  40. 33 22
      my/order/refundDetail.vue
  41. 1 1
      my/order/scenic/refundDetail.vue
  42. 1 1
      my/order/signUp/refundDetail.vue
  43. 17 5
      orderFood/index.vue
  44. 17 0
      pages.json
  45. 23 40
      pages/index/index.vue
  46. 8 5
      pay/pay.vue
  47. 1 1
      refuel/coupon.vue
  48. 7 8
      refuel/refuelDetail.vue
  49. 249 0
      uni_modules/almost-lottery/changelog.md
  50. 1087 0
      uni_modules/almost-lottery/components/almost-lottery/almost-lottery.vue
  51. 85 0
      uni_modules/almost-lottery/package.json
  52. 174 0
      uni_modules/almost-lottery/readme.md
  53. BIN
      uni_modules/almost-lottery/static/almost-lottery/almost-lottery__action.png
  54. BIN
      uni_modules/almost-lottery/static/almost-lottery/almost-lottery__action2x.png
  55. BIN
      uni_modules/almost-lottery/static/almost-lottery/almost-lottery__action3x.png
  56. BIN
      uni_modules/almost-lottery/static/almost-lottery/almost-lottery__bg.png
  57. BIN
      uni_modules/almost-lottery/static/almost-lottery/almost-lottery__bg2x.png
  58. BIN
      uni_modules/almost-lottery/static/almost-lottery/almost-lottery__bg3x.png
  59. 298 0
      uni_modules/almost-lottery/utils/almost-utils.js

+ 11 - 1
api/goods.js

@@ -112,4 +112,14 @@ export function goodsBuyStatistic(data) {
 		method:'post',
 		data
 	})
-}
+}
+
+// 商品类型查询
+export function getGoodsList(data) {
+	return request({
+		url: `/productcenterserver/search/v2/find?productType=${data.productType}&regionCode=${data.regionCode }&page=${data.page}&size=${data.size}`,
+		method:'post',
+		data
+	})
+}
+

+ 76 - 0
api/luckyDraw.js

@@ -0,0 +1,76 @@
+/*
+ * @Author: wenjie 1454560336@qq.com
+ * @Date: 2024-09-18 10:57:18
+ * @LastEditors: wenjie 1454560336@qq.com
+ * @LastEditTime: 2024-09-20 09:35:10
+ * @FilePath: \admin-manage\src\api\luckyDraw.js
+ * @Description: 
+ * 
+ * Copyright (c) 2024 by ${git_name_email}, All Rights Reserved. 
+ */
+import {request} from '@/utils/request.js'
+
+  
+// 抽奖详情
+export function getLuckyDrawDetail(data) {
+    return request({
+      url: '/zswl-cloud-shop/raffleInfo/detail',
+      method: 'get',
+      data
+    })
+  }
+
+// 抽奖规则
+export function luckyDrawRule(data) {
+  return request({
+    url: '/zswl-cloud-shop/raffleInfo/rule',
+    method: 'get',
+    data
+  })
+}
+
+// 中奖数据
+export function prizeList(data) {
+  return request({
+    url: '/zswl-cloud-shop/raffleInfo/prizeList',
+    method: 'get',
+    data
+  })
+}
+
+// 抽奖
+export function prize(data) {
+  return request({
+    url: '/zswl-cloud-shop/raffleInfo/prize',
+    method: 'post',
+    data
+  })
+}
+
+// 视频会员发放
+export function sendVipPrize(data) {
+  return request({
+    url: '/zswl-cloud-shop/raffleInfo/vip',
+    method: 'post',
+    data
+  })
+}
+
+// 邮寄信息填写
+export function notes(data) {
+  return request({
+    url: '/zswl-cloud-shop/raffleInfo/notes',
+    method: 'post',
+    data
+  })
+}
+
+// 抽奖次数
+export function remainder(data) {
+  return request({
+    url: '/zswl-cloud-shop/raffleInfo/remainder',
+    data
+  })
+}
+
+

BIN
components/.DS_Store


BIN
components/@lucky-canvas/.DS_Store


+ 201 - 0
components/@lucky-canvas/uni/LICENSE

@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [2021] [Li Dong Qi]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 138 - 0
components/@lucky-canvas/uni/README.md

@@ -0,0 +1,138 @@
+<br />
+
+<div align="center">
+  <img src="https://cdn.jsdelivr.net/gh/buuing/cdn/imgs/lucky-canvas.jpg" width="210" alt="logo" />
+  <h1>lucky-canvas 抽奖插件</h1>
+  <p>一个基于 JavaScript 的跨平台 ( 大转盘 / 九宫格 / 老虎机 ) 抽奖插件</p>
+  <p>
+    <a href="https://github.com/buuing/lucky-canvas/stargazers" target="_black">
+      <img src="https://img.shields.io/github/stars/buuing/lucky-canvas?color=%23ffba15&logo=github&style=flat-square" alt="stars" />
+    </a>
+    <a href="https://github.com/buuing/lucky-canvas/network/members" target="_black">
+      <img src="https://img.shields.io/github/forks/buuing/lucky-canvas?color=%23ffba15&logo=github&style=flat-square" alt="forks" />
+    </a>
+    <a href="https://github.com/buuing" target="_black">
+      <img src="https://img.shields.io/badge/Author-%20buuing%20-7289da.svg?&logo=github&style=flat-square" alt="author" />
+    </a>
+    <a href="https://github.com/buuing/lucky-canvas/blob/master/LICENSE" target="_black">
+      <img src="https://img.shields.io/github/license/buuing/lucky-canvas?color=%232dce89&logo=github&style=flat-square" alt="license" />
+    </a>
+  </p>
+</div>
+
+
+|适配框架|npm下载量|CDN使用量|
+| :-: | :-: | :-: |
+|[`JS` / `JQ` 中使用](https://100px.net/usage/js.html)|<a href="https://www.npmjs.com/package/lucky-canvas" target="_black"><img src="https://img.shields.io/npm/dm/lucky-canvas?color=%23ffba15&logo=npm&style=flat-square" alt="downloads" /></a>|<a href="https://www.jsdelivr.com/package/npm/lucky-canvas" target="_black"><img src="https://data.jsdelivr.com/v1/package/npm/lucky-canvas/badge" alt="downloads" /></a>|
+|[`Vue` 中使用](https://100px.net/usage/vue.html)|<a href="https://www.npmjs.com/package/@lucky-canvas/vue" target="_black"><img src="https://img.shields.io/npm/dm/@lucky-canvas/vue?color=%23ffba15&logo=npm&style=flat-square" alt="downloads" /></a>|<a href="https://www.jsdelivr.com/package/npm/@lucky-canvas/vue" target="_black"><img src="https://data.jsdelivr.com/v1/package/npm/@lucky-canvas/vue/badge" alt="downloads" /></a>|
+|[`React` 中使用](https://100px.net/usage/react.html)|<a href="https://www.npmjs.com/package/@lucky-canvas/react" target="_black"><img src="https://img.shields.io/npm/dm/@lucky-canvas/react?color=%23ffba15&logo=npm&style=flat-square" alt="downloads" /></a>|-|
+|[`UniApp` 中使用](https://100px.net/usage/uni.html)|<a href="https://www.npmjs.com/package/@lucky-canvas/uni" target="_black"><img src="https://img.shields.io/npm/dm/@lucky-canvas/uni?color=%23ffba15&logo=npm&style=flat-square" alt="downloads" /></a>|-|
+|[`Taro3.x` 中使用](https://100px.net/usage/taro.html)|<a href="https://www.npmjs.com/package/@lucky-canvas/taro" target="_black"><img src="https://img.shields.io/npm/dm/@lucky-canvas/taro?color=%23ffba15&logo=npm&style=flat-square" alt="downloads" /></a>|-|
+|[`微信小程序` 中使用](https://100px.net/usage/wx.html)|<a href="https://www.npmjs.com/package/@lucky-canvas/mini" target="_black"><img src="https://img.shields.io/npm/dm/@lucky-canvas/mini?color=%23ffba15&logo=npm&style=flat-square" alt="downloads" /></a>|-|
+
+<br />
+
+## 官方文档 & Demo演示
+
+> **中文**:[https://100px.net](https://100px.net)
+
+> **English**:**If anyone can help translate the document, please contact me** `ldq404@qq.com`
+  
+<br />
+
+## 在 uni-app 中使用
+
+### 1. 安装插件
+
+- 你可以选择通过 `HBuilderX` 导入插件: [https://ext.dcloud.net.cn/plugin?id=3499](https://ext.dcloud.net.cn/plugin?id=3499)
+
+- 也可以选择通过 `npm` / `yarn` 安装
+
+```shell
+# npm 安装:
+npm install @lucky-canvas/uni
+
+# yarn 安装:
+yarn add @lucky-canvas/uni
+```
+
+<br />
+
+### 2. 引入并使用
+
+```html
+<view>
+  <!-- 大转盘抽奖 -->
+  <LuckyWheel
+    width="600rpx"
+    height="600rpx"
+    ...你的配置
+  />
+  <!-- 九宫格抽奖 -->
+  <LuckyGrid
+    width="600rpx"
+    height="600rpx"
+    ...你的配置
+  />
+</view>
+```
+
+```js
+// npm 下载会默认到 node_modules 里面,直接引入包名即可
+import LuckyWheel from '@lucky-canvas/uni/lucky-wheel' // 大转盘
+import LuckyGrid from '@lucky-canvas/uni/lucky-grid' // 九宫格
+
+// 如果你是通过 HBuilderX 导入插件,那你需要指定一下路径
+// import LuckyWheel from '@/components/@lucky-canvas/uni/lucky-wheel' // 大转盘
+// import LuckyGrid from '@/components/@lucky-canvas/uni/lucky-grid' // 九宫格
+
+export default {
+  // 注册组件
+  components: { LuckyWheel, LuckyGrid },
+}
+```
+
+<br />
+
+### 3. 我提供了一个最基本的 demo 供你用于尝试
+
+由于 uni-app 渲染 md 的时候会出问题,所以我把 demo 代码放到了文档里
+
+- [https://100px.net/document/uni-app.html](https://100px.net/document/uni-app.html)
+
+<br />
+
+### **4. 补充说明**
+
+- [**如果用着顺手, 可以在 Github 上面点个 <img height="22" align="top" src="https://img.shields.io/github/stars/buuing/lucky-canvas" /> 支持一下(●'◡'●)**](https://github.com/buuing/lucky-canvas)
+
+- 另外: 如果你修复了某些bug或兼容, 欢迎提给我, 我会把你展示到官网的贡献者列表当中
+
+
+<br />
+
+### 5. 常见问题
+
+1. 转盘层级太高了, 我的弹窗盖不住怎么办?
+
+> 答: 因为小程序里canvas是原生组件顶层渲染, 我无法控制canvas的层级, 如果你想盖住它也肯简单, 你可以百度搜索`<cover>`组件
+
+2. 你这些素材, 图片组件从哪下载?
+
+> 答: 官网里的任何图片素材, 所使用到的图片资源均为学习交流使用, 请勿将其用于商业用途, 由此产生的任何商业纠纷我这边概不负责
+
+3. xxx属性怎么使用? xxx方法怎么调用?
+
+> 答: 自己去看文档, 不然难道要我把代码给你写好吗?
+
+4. 这个属性的效果与官网的描述不一致?
+
+> 答: 可能有bug, 你可以去github上的issues去提问 (请认真填写模板)
+
+5. 为什么这个插件不支持app和其他小程序
+
+> 答: 没时间, 但是希望志同道合的同学来一起参与uniapp的兼容开发
+
+---
+
+<font color="blue">作者留言: 为了使我自己保持心情愉悦, 低于5星的提问我用浏览器插件都屏蔽了</font>

+ 317 - 0
components/@lucky-canvas/uni/lucky-grid.vue

@@ -0,0 +1,317 @@
+<template>
+  <view v-if="isShow" class="lucky-box" :style="{ width: boxWidth + 'px', height: boxHeight + 'px' }">
+    <canvas
+      type="2d"
+      id="lucky-grid"
+      canvas-id="lucky-grid"
+      :style="{ width: boxWidth + 'px', height: boxHeight + 'px' }"
+    ></canvas>
+    <image
+      v-if="imgSrc"
+      :src="imgSrc"
+      @load="myLucky.clearCanvas()"
+      :style="{ width: boxWidth + 'px', height: boxHeight + 'px' }"
+    ></image>
+    <!-- #ifdef APP-PLUS -->
+    <view v-if="btnShow">
+      <view class="lucky-grid-btn" v-for="(btn, index) in btns" :key="index" @click="toPlay(btn, index)" :style="{
+        top: btn.top + 'px',
+        left: btn.left + 'px',
+        width: btn.width + 'px',
+        height: btn.height + 'px',
+      }"></view>
+    </view>
+    <!-- #endif -->
+    <!-- #ifndef APP-PLUS -->
+    <view v-if="btnShow">
+      <cover-view class="lucky-grid-btn" v-for="(btn, index) in btns" :key="index" @click="toPlay(btn, index)" :style="{
+        top: btn.top + 'px',
+        left: btn.left + 'px',
+        width: btn.width + 'px',
+        height: btn.height + 'px',
+      }"></cover-view>
+    </view>
+    <!-- #endif -->
+    <!-- #ifndef H5 -->
+    <view v-if="myLucky">
+      <div class="lucky-imgs">
+        <div v-for="(block, index) in blocks" :key="index">
+          <div v-if="block.imgs">
+            <div v-for="(img, i) in block.imgs" :key="i">
+              <image :src="img.src" :data-index="index" :data-i="i" @load="e => imgBindload(e, 'blocks')"></image>
+              <image :src="img.activeSrc" :data-index="index" :data-i="i" @load="e => imgBindloadActive(e, 'blocks')"></image>
+            </div>
+          </div>
+        </div>
+      </div>
+      <div class="lucky-imgs">
+        <div v-for="(prize, index) in prizes" :key="index">
+          <div v-if="prize.imgs">
+            <div v-for="(img, i) in prize.imgs" :key="i">
+              <image :src="img.src" :data-index="index" :data-i="i" @load="e => imgBindload(e, 'prizes')"></image>
+              <image :src="img.activeSrc" :data-index="index" :data-i="i" @load="e => imgBindloadActive(e, 'prizes')"></image>
+            </div>
+          </div>
+        </div>
+      </div>
+      <div class="lucky-imgs">
+        <div v-for="(btn, index) in buttons" :key="index">
+          <div v-if="btn.imgs">
+            <image v-for="(img, i) in btn.imgs" :key="i" :src="img.src" :data-index="index" :data-i="i" @load="e => imgBindload(e, 'buttons')"></image>
+          </div>
+        </div>
+      </div>
+      <div class="lucky-imgs">
+        <span v-if="button && button.imgs">
+          <image v-for="(img, i) in button.imgs" :key="i" :src="img.src" :data-i="i" @load="e => imgBindloadBtn(e, 'button')"></image>
+        </span>
+      </div>
+    </view>
+    <!-- #endif -->
+  </view>
+</template>
+
+<script>
+  import { changeUnits, resolveImage, getImage } from './utils.js'
+  import { LuckyGrid } from '../../lucky-canvas'
+  export default {
+    name: 'lucky-grid',
+    data () {
+      return {
+        imgSrc: '',
+        myLucky: null,
+        canvas: null,
+        isShow: false,
+        boxWidth: 100,
+        boxHeight: 100,
+        dpr: 1,
+        btns: [],
+        btnShow: false,
+      }
+    },
+    props: {
+      width: {
+        type: String,
+        default: '600rpx'
+      },
+      height: {
+        type: String,
+        default: '600rpx'
+      },
+      cols: {
+        type: [String, Number],
+        default: 3,
+      },
+      rows: {
+        type: [String, Number],
+        default: 3,
+      },
+      blocks: {
+        type: Array,
+        default: () => []
+      },
+      prizes: {
+        type: Array,
+        default: () => []
+      },
+      buttons: {
+        type: Array,
+        default: () => []
+      },
+      button: {
+        type: Object,
+        default: undefined
+      },
+      defaultConfig: {
+        type: Object,
+        default: () => ({})
+      },
+      defaultStyle: {
+        type: Object,
+        default: () => ({})
+      },
+      activeStyle: {
+        type: Object,
+        default: () => ({})
+      }
+    },
+    mounted () {
+      // #ifdef APP-PLUS
+      console.error('该抽奖插件的最新版暂不支持app端, 请通过npm安装旧版本【npm i uni-luck-draw@1.3.9】')
+      // #endif
+      // #ifndef APP-PLUS
+      this.initLucky()
+      // #endif
+    },
+    watch: {
+      cols (newData) {
+        this.myLucky && (this.myLucky.cols = newData)
+      },
+      rows (newData) {
+        this.myLucky && (this.myLucky.rows = newData)
+      },
+      blocks (newData) {
+        this.myLucky && (this.myLucky.blocks = newData)
+      },
+      prizes (newData) {
+        this.myLucky && (this.myLucky.prizes = newData)
+      },
+      buttons (newData) {
+        this.myLucky && (this.myLucky.buttons = newData)
+      },
+      button (newData) {
+        this.myLucky && (this.myLucky.button = newData)
+      },
+      defaultStyle (newData) {
+        this.myLucky && (this.myLucky.defaultStyle = newData)
+      },
+      defaultConfig (newData) {
+        this.myLucky && (this.myLucky.defaultConfig = newData)
+      },
+      activeStyle (newData) {
+        this.myLucky && (this.myLucky.activeStyle = newData)
+      },
+    },
+    methods: {
+      async imgBindload (res, name) {
+        const { index, i } = res.currentTarget.dataset
+        const img = this[name][index].imgs[i]
+        resolveImage(img, this.canvas)
+      },
+      async imgBindloadActive (res, name) {
+        const { index, i } = res.currentTarget.dataset
+        const img = this[name][index].imgs[i]
+        resolveImage(img, this.canvas, 'activeSrc', '$activeResolve')
+      },
+      async imgBindloadBtn (res, name) {
+        const { i } = res.currentTarget.dataset
+        const img = this[name].imgs[i]
+        resolveImage(img, this.canvas)
+      },
+      getImage () {
+        return getImage.call(this, 'lucky-grid', this.canvas)
+      },
+      hideCanvas () {
+        // #ifdef MP
+        this.getImage().then(res => {
+          this.imgSrc = res.tempFilePath
+        })
+        // #endif
+      },
+      initLucky () {
+        this.boxWidth = changeUnits(this.width)
+        this.boxHeight = changeUnits(this.height)
+        this.isShow = true
+        // 某些情况下获取不到 canvas
+        this.$nextTick(() => {
+          setTimeout(() => {
+            this.draw()
+          })
+        })
+      },
+      draw () {
+        const _this = this
+        uni.createSelectorQuery().in(this).select('#lucky-grid').fields({
+          node: true, size: true
+        }).exec((res) => {
+          // #ifdef H5
+          res[0].node = document.querySelector('#lucky-grid canvas')
+          // #endif
+          if (!res[0] || !res[0].node) return console.error('lucky-canvas 获取不到 canvas 标签')
+          const { node, width, height } = res[0]
+          const canvas = this.canvas = node
+          const ctx = this.ctx = canvas.getContext('2d')
+          const dpr = this.dpr = uni.getSystemInfoSync().pixelRatio
+          // #ifndef H5
+          canvas.width = width * dpr
+          canvas.height = height * dpr
+          ctx.scale(dpr, dpr)
+          // #endif
+          const myLucky = this.myLucky = new LuckyGrid({
+            // #ifdef H5
+            flag: 'WEB',
+            // #endif
+            // #ifdef MP
+            flag: 'MP-WX',
+            // #endif
+            ctx,
+            dpr,
+            setTimeout,
+            clearTimeout,
+            setInterval,
+            clearInterval,
+            // #ifdef H5
+            rAF: requestAnimationFrame,
+            // #endif
+            unitFunc: (num, unit) => changeUnits(num + unit),
+            afterInit: function () {
+              [..._this.$props.buttons, _this.$props.button].forEach((btn, index) => {
+                if (!btn) return
+                const [left, top, width, height] = this.getGeometricProperty([
+                  btn.x,
+                  btn.y,
+                  btn.col || 1,
+                  btn.row || 1
+                ])
+                _this.btns[index] = { top, left, width, height }
+              })
+              _this.$forceUpdate()
+            },
+            afterStart: () => {
+              this.imgSrc = ''
+            },
+          }, {
+            ...this.$props,
+            width,
+            height,
+            start: (...rest) => {
+              this.$emit('start', ...rest)
+            },
+            end: (...rest) => {
+              this.$emit('end', ...rest)
+              this.hideCanvas()
+            },
+          })
+          this.btnShow = true
+        })
+      },
+      toPlay (btn, index) {
+        this.myLucky.startCallback(btn, this.$props.buttons[index])
+      },
+      init () {
+        this.myLucky.init()
+      },
+      play (...rest) {
+        this.myLucky.play(...rest)
+      },
+      stop (...rest) {
+        this.myLucky.stop(...rest)
+      },
+    },
+  }
+</script>
+
+<style scoped>
+  .lucky-box {
+    position: relative;
+    overflow: hidden;
+    margin: 0 auto;
+  }
+  .lucky-box canvas {
+    position: absolute;
+    pointer-events: none;
+    left: 0;
+    top: 0;
+  }
+  .lucky-grid-btn {
+    position: absolute;
+    background: rgba(0, 0, 0, 0);
+    border-radius: 0;
+    cursor: pointer;
+  }
+  .lucky-imgs {
+    width: 0;
+    height: 0;
+    visibility: hidden;
+  }
+</style>

+ 255 - 0
components/@lucky-canvas/uni/lucky-wheel.vue

@@ -0,0 +1,255 @@
+<template>
+  <view v-if="isShow" class="lucky-box" :style="{ width: boxWidth + 'px', height: boxHeight + 'px' }">
+    <canvas
+      type="2d"
+      id="lucky-wheel"
+      canvas-id="lucky-wheel"
+      :style="{ width: boxWidth + 'px', height: boxHeight + 'px' }"
+    ></canvas>
+    <image
+      v-if="imgSrc"
+      :src="imgSrc"
+      @load="myLucky.clearCanvas()"
+      :style="{ width: boxWidth + 'px', height: boxHeight + 'px' }"
+    ></image>
+    <!-- #ifdef APP-PLUS -->
+    <view class="lucky-wheel-btn" @click="toPlay" :style="{ width: btnWidth + 'px', height: btnHeight + 'px' }"></view>
+    <!-- #endif -->
+    <!-- #ifndef APP-PLUS -->
+    <cover-view class="lucky-wheel-btn" @click="toPlay" :style="{ width: btnWidth + 'px', height: btnHeight + 'px' }"></cover-view>
+    <!-- #endif -->
+    <!-- #ifndef H5 -->
+    <view v-if="myLucky">
+      <div class="lucky-imgs">
+        <div v-for="(block, index) in blocks" :key="index">
+          <div v-if="block.imgs">
+            <image v-for="(img, i) in block.imgs" :key="i" :src="img.src" @load="e => imgBindload(e, 'blocks', index, i)"></image>
+          </div>
+        </div>
+      </div>
+      <div class="lucky-imgs">
+        <div v-for="(prize, index) in prizes" :key="index">
+          <div v-if="prize.imgs">
+            <image v-for="(img, i) in prize.imgs" :key="i" :src="img.src" @load="e => imgBindload(e, 'prizes', index, i)"></image>
+          </div>
+        </div>
+      </div>
+      <div class="lucky-imgs">
+        <div v-for="(btn, index) in buttons" :key="index">
+          <div v-if="btn.imgs">
+            <image v-for="(img, i) in btn.imgs" :key="i" :src="img.src" @load="e => imgBindload(e, 'buttons', index, i)"></image>
+          </div>
+        </div>
+      </div>
+    </view>
+    <!-- #endif -->
+  </view>
+</template>
+
+<script>
+  import { changeUnits, resolveImage, getImage } from './utils.js'
+  import { LuckyWheel } from '../../lucky-canvas'
+  export default {
+    name: 'lucky-wheel',
+    data () {
+      return {
+        imgSrc: '',
+        myLucky: null,
+        canvas: null,
+        isShow: false,
+        boxWidth: 100,
+        boxHeight: 100,
+        btnWidth: 0,
+        btnHeight: 0,
+        dpr: 1,
+      }
+    },
+    props: {
+      width: {
+        type: String,
+        default: '600rpx'
+      },
+      height: {
+        type: String,
+        default: '600rpx'
+      },
+      blocks: {
+        type: Array,
+        default: () => []
+      },
+      prizes: {
+        type: Array,
+        default: () => []
+      },
+      buttons: {
+        type: Array,
+        default: () => []
+      },
+      defaultConfig: {
+        type: Object,
+        default: () => ({})
+      },
+      defaultStyle: {
+        type: Object,
+        default: () => ({})
+      },
+    },
+    mounted () {
+      // #ifdef APP-PLUS
+      console.error('该抽奖插件的最新版暂不支持app端, 请通过npm安装旧版本【npm i uni-luck-draw@1.3.9】')
+      // #endif
+      // #ifndef APP-PLUS
+      this.initLucky()
+      // #endif
+    },
+    watch: {
+      blocks (newData) {
+        this.myLucky && (this.myLucky.blocks = newData)
+      },
+      prizes (newData) {
+        this.myLucky && (this.myLucky.prizes = newData)
+      },
+      buttons (newData) {
+        this.myLucky && (this.myLucky.buttons = newData)
+      },
+      defaultStyle (newData) {
+        this.myLucky && (this.myLucky.defaultStyle = newData)
+      },
+      defaultConfig (newData) {
+        this.myLucky && (this.myLucky.defaultConfig = newData)
+      },
+    },
+    methods: {
+      async imgBindload (res, name, index, i) {
+        const img = this[name][index].imgs[i]
+        resolveImage(img, this.canvas)
+      },
+      getImage () {
+        return getImage.call(this, 'lucky-wheel', this.canvas)
+      },
+      hideCanvas () {
+        // #ifdef MP
+        this.getImage().then(res => {
+          this.imgSrc = res.tempFilePath
+        })
+        // #endif
+      },
+      initLucky () {
+        this.boxWidth = changeUnits(this.width)
+        this.boxHeight = changeUnits(this.height)
+        this.isShow = true
+        // 某些情况下获取不到 canvas
+        this.$nextTick(() => {
+          setTimeout(() => {
+            this.draw()
+          })
+        })
+      },
+      draw () {
+        const _this = this
+        uni.createSelectorQuery().in(this).select('#lucky-wheel').fields({
+          node: true, size: true
+        }).exec((res) => {
+          // #ifdef H5
+          res[0].node = document.querySelector('#lucky-wheel canvas')
+          // #endif
+          if (!res[0] || !res[0].node) return console.error('lucky-canvas 获取不到 canvas 标签')
+          const { node, width, height } = res[0]
+          const canvas = this.canvas = node
+          const ctx = this.ctx = canvas.getContext('2d')
+          const dpr = this.dpr = uni.getSystemInfoSync().pixelRatio
+          // #ifndef H5
+          canvas.width = width * dpr
+          canvas.height = height * dpr
+          ctx.scale(dpr, dpr)
+          // #endif
+          const Radius = Math.min(width, height) / 2
+          const myLucky = this.myLucky = new LuckyWheel({
+            // #ifdef H5
+            flag: 'WEB',
+            // #endif
+            // #ifdef MP
+            flag: 'MP-WX',
+            // #endif
+            ctx,
+            dpr,
+            setTimeout,
+            clearTimeout,
+            setInterval,
+            clearInterval,
+            // #ifdef H5
+            rAF: requestAnimationFrame,
+            // #endif
+            unitFunc: (num, unit) => changeUnits(num + unit),
+            beforeCreate: function () {
+              ctx.translate(Radius, Radius)
+            },
+            beforeResize: function () {
+              ctx.translate(-Radius, -Radius)
+            },
+            afterInit: function () {
+              // 动态设置按钮
+              _this.btnWidth = this.maxBtnRadius * 2
+              _this.btnHeight = this.maxBtnRadius * 2
+              _this.$forceUpdate()
+            },
+            afterStart: () => {
+              this.imgSrc = ''
+            },
+          }, {
+            ...this.$props,
+            width,
+            height,
+            start: (...rest) => {
+              this.$emit('start', ...rest)
+            },
+            end: (...rest) => {
+              this.$emit('end', ...rest)
+              this.hideCanvas()
+            },
+          })
+        })
+      },
+      toPlay (e) {
+        this.myLucky.startCallback()
+      },
+      init () {
+        this.myLucky.init()
+      },
+      play (...rest) {
+        this.myLucky.play(...rest)
+      },
+      stop (...rest) {
+        this.myLucky.stop(...rest)
+      },
+    },
+  }
+</script>
+
+<style scoped>
+  .lucky-box {
+    position: relative;
+    overflow: hidden;
+    margin: 0 auto;
+  }
+  .lucky-box canvas {
+    position: absolute;
+    pointer-events: none;
+    left: 0;
+    top: 0;
+  }
+  .lucky-wheel-btn {
+    position: absolute;
+    left: 50%;
+    top: 50%;
+    transform: translate(-50%, -50%);
+    background: rgba(0, 0, 0, 0);
+    border-radius: 50%;
+    cursor: pointer;
+  }
+  .lucky-imgs {
+    width: 0;
+    height: 0;
+    visibility: hidden;
+  }
+</style>

+ 23 - 0
components/@lucky-canvas/uni/package.json

@@ -0,0 +1,23 @@
+{
+  "name": "@lucky-canvas/uni",
+  "version": "0.0.10",
+  "description": "uni-app【大转盘 / 九宫格 / 老虎机】抽奖插件",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "keywords": [
+    "uni-app抽奖"
+  ],
+  "files": [
+    "lucky-wheel.vue",
+    "lucky-grid.vue",
+    "slot-machine.vue",
+    "utils.js",
+    "demo.vue"
+  ],
+  "author": "ldq <ldq404@qq.com>",
+  "license": "Apache-2.0",
+  "dependencies": {
+    "lucky-canvas": "~1.7.19"
+  }
+}

+ 214 - 0
components/@lucky-canvas/uni/slot-machine.vue

@@ -0,0 +1,214 @@
+<template>
+  <view v-if="isShow" class="lucky-box" :style="{ width: boxWidth + 'px', height: boxHeight + 'px' }">
+    <canvas
+      type="2d"
+      id="slot-machine"
+      canvas-id="slot-machine"
+      :style="{ width: boxWidth + 'px', height: boxHeight + 'px' }"
+    ></canvas>
+    <image
+      v-if="imgSrc"
+      :src="imgSrc"
+      @load="myLucky.clearCanvas()"
+      :style="{ width: boxWidth + 'px', height: boxHeight + 'px' }"
+    ></image>
+    <!-- #ifndef H5 -->
+    <view v-if="myLucky">
+      <div class="lucky-imgs">
+        <div v-for="(block, index) in blocks" :key="index">
+          <div v-if="block.imgs">
+            <image v-for="(img, i) in block.imgs" :key="i" :src="img.src" @load="e => imgBindload(e, 'blocks', index, i)"></image>
+          </div>
+        </div>
+      </div>
+      <div class="lucky-imgs">
+        <div v-for="(prize, index) in prizes" :key="index">
+          <div v-if="prize.imgs">
+            <image v-for="(img, i) in prize.imgs" :key="i" :src="img.src" @load="e => imgBindload(e, 'prizes', index, i)"></image>
+          </div>
+        </div>
+      </div>
+    </view>
+    <!-- #endif -->
+  </view>
+</template>
+
+<script>
+  import { changeUnits, resolveImage, getImage } from './utils.js'
+  import { SlotMachine } from '../../lucky-canvas'
+  export default {
+    name: 'slot-machine',
+    data () {
+      return {
+        imgSrc: '',
+        myLucky: null,
+        canvas: null,
+        isShow: false,
+        boxWidth: 100,
+        boxHeight: 100,
+        btnWidth: 0,
+        btnHeight: 0,
+        dpr: 1,
+      }
+    },
+    props: {
+      width: {
+        type: String,
+        default: '600rpx'
+      },
+      height: {
+        type: String,
+        default: '600rpx'
+      },
+      blocks: {
+        type: Array,
+        default: () => []
+      },
+      prizes: {
+        type: Array,
+        default: () => []
+      },
+      slots: {
+        type: Array,
+        default: () => []
+      },
+      defaultConfig: {
+        type: Object,
+        default: () => ({})
+      },
+      defaultStyle: {
+        type: Object,
+        default: () => ({})
+      },
+    },
+    mounted () {
+      // #ifndef APP-PLUS
+      this.initLucky()
+      // #endif
+    },
+    watch: {
+      blocks (newData) {
+        this.myLucky && (this.myLucky.blocks = newData)
+      },
+      prizes (newData) {
+        this.myLucky && (this.myLucky.prizes = newData)
+      },
+      slots (newData) {
+        this.myLucky && (this.myLucky.slots = newData)
+      },
+      defaultStyle (newData) {
+        this.myLucky && (this.myLucky.defaultStyle = newData)
+      },
+      defaultConfig (newData) {
+        this.myLucky && (this.myLucky.defaultConfig = newData)
+      },
+    },
+    methods: {
+      async imgBindload (res, name, index, i) {
+        const img = this[name][index].imgs[i]
+        resolveImage(img, this.canvas)
+      },
+      getImage () {
+        return getImage.call(this, 'slot-machine', this.canvas)
+      },
+      hideCanvas () {
+        // #ifdef MP
+        this.getImage().then(res => {
+          this.imgSrc = res.tempFilePath
+        })
+        // #endif
+      },
+      initLucky () {
+        this.boxWidth = changeUnits(this.width)
+        this.boxHeight = changeUnits(this.height)
+        this.isShow = true
+        // 某些情况下获取不到 canvas
+        this.$nextTick(() => {
+          setTimeout(() => {
+            this.draw()
+          })
+        })
+      },
+      draw () {
+        const _this = this
+        uni.createSelectorQuery().in(this).select('#slot-machine').fields({
+          node: true, size: true
+        }).exec((res) => {
+          // #ifdef H5
+          res[0].node = document.querySelector('#slot-machine canvas')
+          // #endif
+          if (!res[0] || !res[0].node) return console.error('lucky-canvas 获取不到 canvas 标签')
+          const { node, width, height } = res[0]
+          const canvas = this.canvas = node
+          const ctx = this.ctx = canvas.getContext('2d')
+          const dpr = this.dpr = uni.getSystemInfoSync().pixelRatio
+          // #ifndef H5
+          canvas.width = width * dpr
+          canvas.height = height * dpr
+          ctx.scale(dpr, dpr)
+          // #endif
+          const myLucky = this.myLucky = new SlotMachine({
+            // #ifdef H5
+            flag: 'WEB',
+            // #endif
+            // #ifdef MP
+            flag: 'MP-WX',
+            // #endif
+            ctx,
+            dpr,
+            // #ifndef H5
+            offscreenCanvas: uni.createOffscreenCanvas({ type: '2d' }),
+            // #endif
+            setTimeout,
+            clearTimeout,
+            setInterval,
+            clearInterval,
+            // #ifdef H5
+            rAF: requestAnimationFrame,
+            // #endif
+            unitFunc: (num, unit) => changeUnits(num + unit),
+            afterStart: () => {
+              this.imgSrc = ''
+            },
+          }, {
+            ...this.$props,
+            width,
+            height,
+            end: (...rest) => {
+              this.$emit('end', ...rest)
+              this.hideCanvas()
+            },
+          })
+        })
+      },
+      init () {
+        this.myLucky.init()
+      },
+      play (...rest) {
+        this.myLucky.play(...rest)
+      },
+      stop (...rest) {
+        this.myLucky.stop(...rest)
+      },
+    },
+  }
+</script>
+
+<style scoped>
+  .lucky-box {
+    position: relative;
+    overflow: hidden;
+    margin: 0 auto;
+  }
+  .lucky-box canvas {
+    position: absolute;
+    pointer-events: none;
+    left: 0;
+    top: 0;
+  }
+  .lucky-imgs {
+    width: 0;
+    height: 0;
+    visibility: hidden;
+  }
+</style>

+ 82 - 0
components/@lucky-canvas/uni/utils.js

@@ -0,0 +1,82 @@
+let windowWidth = uni.getSystemInfoSync().windowWidth
+// uni-app@2.9起, 屏幕最多适配到960, 超出则按375计算
+if (windowWidth > 960) windowWidth = 375
+
+export const rpx2px = (value) => {
+  if (typeof value === 'string') value = Number(value.replace(/[a-z]*/g, ''))
+  return windowWidth / 750 * value
+}
+
+export const changeUnits = (value) => {
+  return Number(value.replace(/^(\-*[0-9.]*)([a-z%]*)$/, (value, num, unit) => {
+    switch (unit) {
+      case 'px':
+        num *= 1
+        break
+      case 'rpx':
+        num = rpx2px(num)
+        break
+      default:
+        num *= 1
+        break
+    }
+    return num
+  }))
+}
+
+export const resolveImage = async (img, canvas, srcName = 'src', resolveName = '$resolve') => {
+  let imgObj
+  // 区分 H5 和小程序
+  if (window) {
+    imgObj = new Image()
+  } else {
+    imgObj = canvas.createImage()
+  }
+  // 成功回调
+  imgObj.onload = () => {
+    img[resolveName](imgObj)
+  }
+  // 失败回调
+  imgObj.onerror = (err) => {
+    console.error(err)
+    // img['$reject']()
+  }
+  // 设置src
+  imgObj.src = img[srcName]
+}
+
+// 旧版canvas引入图片的方法
+// export const resolveImage = async (res, img, imgName = 'src', resolveName = '$resolve') => {
+//   const src = img[imgName]
+//   const $resolve = img[resolveName]
+//   // #ifdef MP
+//   // 如果是base64就调用base64src()方法把图片写入本地, 然后渲染临时路径
+//   if (/^data:image\/([a-z]+);base64,/.test(src)) {
+//     const path = await base64src(src)
+//     $resolve({ ...res.detail, path })
+//     return
+//   }
+//   // #endif
+//   // 如果是本地图片, 直接返回
+//   if (src.indexOf('http') !== 0) {
+//     $resolve({ ...res.detail, path:src })
+//     return
+//   }
+//   // 如果是网络图片, 则通过getImageInfo()方法获取图片宽高
+//   uni.getImageInfo({
+//     src: src,
+//     success: (imgObj) => $resolve(imgObj),
+//     fail: () => console.error('API `uni.getImageInfo` 加载图片失败', src)
+//   })
+// }
+
+export function getImage(canvasId, canvas) {
+  return new Promise((resolve, reject) => {
+    uni.canvasToTempFilePath({
+      canvas,
+      canvasId,
+      success: res => resolve(res),
+      fail: err => reject(err)
+    }, this)
+  })
+}

+ 201 - 0
components/lucky-canvas/LICENSE

@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [2021] [Li Dong Qi]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 42 - 0
components/lucky-canvas/README.md

@@ -0,0 +1,42 @@
+
+
+<div align="center">
+  <img src="https://unpkg.com/buuing@0.0.1/imgs/lucky-canvas.png" width="128" alt="logo" />
+  <h1>lucky-canvas 抽奖插件</h1>
+  <p>一个基于 JavaScript 的跨平台 ( 大转盘 / 九宫格 / 老虎机 ) 抽奖插件</p>
+  <p>
+    <a href="https://github.com/buuing/lucky-canvas/stargazers" target="_black">
+      <img src="https://img.shields.io/github/stars/buuing/lucky-canvas?color=%23ffba15&logo=github&style=flat-square" alt="stars" />
+    </a>
+    <a href="https://github.com/buuing/lucky-canvas/network/members" target="_black">
+      <img src="https://img.shields.io/github/forks/buuing/lucky-canvas?color=%23ffba15&logo=github&style=flat-square" alt="forks" />
+    </a>
+    <a href="https://github.com/buuing" target="_black">
+      <img src="https://img.shields.io/badge/Author-%20buuing%20-7289da.svg?&logo=github&style=flat-square" alt="author" />
+    </a>
+    <a href="https://github.com/buuing/lucky-canvas/blob/master/LICENSE" target="_black">
+      <img src="https://img.shields.io/github/license/buuing/lucky-canvas?color=%232dce89&logo=github&style=flat-square" alt="license" />
+    </a>
+  </p>
+</div>
+
+<br />
+
+## 官方文档 & Demo演示
+
+> **中文**:[https://100px.net/usage/js.html](https://100px.net/usage/js.html)  
+
+> **English**:**If anyone can help translate the document, please contact me** `ldq404@qq.com`
+
+
+<br />
+
+## 在 JS / TS 中使用
+
+- [跳转官网 查看详情](https://100px.net/usage/js.html)
+
+<br />
+
+## 🙏🙏🙏 点个Star
+
+**如果您觉得这个项目还不错, 可以在 [Github](https://github.com/buuing/lucky-canvas) 上面帮我点个`star`, 支持一下作者 ☜(゚ヮ゚☜)**

File diff suppressed because it is too large
+ 15 - 0
components/lucky-canvas/dist/index.cjs.js


File diff suppressed because it is too large
+ 0 - 0
components/lucky-canvas/dist/index.cjs.js.map


File diff suppressed because it is too large
+ 14 - 0
components/lucky-canvas/dist/index.esm.js


File diff suppressed because it is too large
+ 0 - 0
components/lucky-canvas/dist/index.esm.js.map


File diff suppressed because it is too large
+ 14 - 0
components/lucky-canvas/dist/index.umd.js


File diff suppressed because it is too large
+ 0 - 0
components/lucky-canvas/dist/index.umd.js.map


File diff suppressed because it is too large
+ 14 - 0
components/lucky-canvas/dist/lucky-canvas.js


+ 1 - 0
components/lucky-canvas/index.js

@@ -0,0 +1 @@
+module.exports = require('./dist/index.umd.js')

+ 66 - 0
components/lucky-canvas/package.json

@@ -0,0 +1,66 @@
+{
+  "name": "lucky-canvas",
+  "version": "1.7.26",
+  "description": "一个基于原生 js 的(大转盘 / 九宫格 / 老虎机)抽奖插件",
+  "main": "dist/index.cjs.js",
+  "module": "dist/index.esm.js",
+  "unpkg": "dist/index.umd.js",
+  "jsdelivr": "dist/index.umd.js",
+  "types": "types/index.d.ts",
+  "scripts": {
+    "dev": "rollup --config rollup.config.dev.js -w",
+    "build": "rollup --config rollup.config.build.js"
+  },
+  "homepage": "https://100px.net",
+  "bugs": "https://github.com/LuckDraw/lucky-canvas/issues",
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/LuckDraw/lucky-canvas.git",
+    "directory": "packages/lucky-canvas"
+  },
+  "author": "ldq <ldq404@qq.com>",
+  "license": "Apache-2.0",
+  "files": [
+    "dist",
+    "types",
+    "index.js"
+  ],
+  "keywords": [
+    "大转盘抽奖",
+    "九宫格抽奖",
+    "老虎机抽奖",
+    "抽奖插件",
+    "js抽奖",
+    "移动端抽奖",
+    "canvas抽奖"
+  ],
+  "devDependencies": {
+    "@babel/core": "^7.12.3",
+    "@babel/preset-env": "^7.12.1",
+    "@babel/plugin-transform-runtime": "^7.16.4",
+    "@babel/runtime": "^7.16.3",
+    "core-js": "^3.19.2",
+    "@rollup/plugin-commonjs": "^16.0.0",
+    "@rollup/plugin-eslint": "^8.0.1",
+    "@rollup/plugin-json": "^4.1.0",
+    "@rollup/plugin-node-resolve": "^10.0.0",
+    "@rollup/plugin-typescript": "^6.1.0",
+    "@typescript-eslint/parser": "^4.14.0",
+    "babel-plugin-external-helpers": "^6.22.0",
+    "babel-preset-latest": "^6.24.1",
+    "eslint": "^7.18.0",
+    "eslint-plugin-prettier": "^3.3.1",
+    "prettier": "^2.2.1",
+    "rollup": "^2.33.1",
+    "rollup-plugin-babel": "^4.4.0",
+    "rollup-plugin-livereload": "^2.0.0",
+    "rollup-plugin-serve": "^1.1.0",
+    "rollup-plugin-terser": "^7.0.2",
+    "rollup-plugin-delete": "^2.0.0",
+    "rollup-plugin-dts": "^3.0.2",
+    "rollup-plugin-typescript2": "^0.30.0",
+    "tslib": "^2.3.1",
+    "typescript": "^4.0.5"
+  },
+  "dependencies": {}
+}

+ 752 - 0
components/lucky-canvas/types/index.d.ts

@@ -0,0 +1,752 @@
+declare type FontItemType = {
+    text: string;
+    top?: string | number;
+    left?: string | number;
+    fontColor?: string;
+    fontSize?: string;
+    fontStyle?: string;
+    fontWeight?: string;
+    lineHeight?: string;
+};
+declare type FontExtendType = {
+    wordWrap?: boolean;
+    lengthLimit?: string | number;
+    lineClamp?: number;
+};
+declare type ImgType = HTMLImageElement | HTMLCanvasElement;
+declare type ImgItemType = {
+    src: string;
+    top?: string | number;
+    left?: string | number;
+    width?: string;
+    height?: string;
+    formatter?: (img: ImgType) => ImgType;
+    $resolve?: Function;
+    $reject?: Function;
+};
+declare type BorderRadiusType = string | number;
+declare type BackgroundType = string;
+declare type ShadowType = string;
+declare type ConfigType = {
+    nodeType?: number;
+    flag: 'WEB' | 'MP-WX' | 'UNI-H5' | 'UNI-MP' | 'TARO-H5' | 'TARO-MP';
+    el?: string;
+    divElement?: HTMLDivElement;
+    canvasElement?: HTMLCanvasElement;
+    ctx: CanvasRenderingContext2D;
+    dpr: number;
+    handleCssUnit?: (num: number, unit: string) => number;
+    rAF?: Function;
+    setTimeout: Function;
+    setInterval: Function;
+    clearTimeout: Function;
+    clearInterval: Function;
+    beforeCreate?: Function;
+    beforeResize?: Function;
+    afterResize?: Function;
+    beforeInit?: Function;
+    afterInit?: Function;
+    beforeDraw?: Function;
+    afterDraw?: Function;
+    afterStart?: Function;
+};
+declare type RequireKey = 'width' | 'height';
+declare type UserConfigType = Partial<Omit<ConfigType, RequireKey>> & Required<Pick<ConfigType, RequireKey>>;
+declare type Tuple<T, Len extends number, Res extends T[] = []> = Res['length'] extends Len ? Res : Tuple<T, Len, [...Res, T]>;
+
+interface WatchOptType {
+    handler?: () => Function;
+    immediate?: boolean;
+    deep?: boolean;
+}
+
+declare class Lucky {
+    static version: string;
+    protected readonly version: string;
+    protected readonly config: ConfigType;
+    protected readonly ctx: CanvasRenderingContext2D;
+    protected htmlFontSize: number;
+    protected rAF: Function;
+    protected boxWidth: number;
+    protected boxHeight: number;
+    protected data: {
+        width: string | number;
+        height: string | number;
+    };
+    /**
+     * 公共构造器
+     * @param config
+     */
+    constructor(config: string | HTMLDivElement | UserConfigType, data: {
+        width: string | number;
+        height: string | number;
+    });
+    /**
+     * 初始化组件大小/单位
+     */
+    protected resize(): void;
+    /**
+     * 初始化方法
+     */
+    protected initLucky(): void;
+    /**
+     * 鼠标点击事件
+     * @param e 事件参数
+     */
+    protected handleClick(e: MouseEvent): void;
+    /**
+     * 根标签的字体大小
+     */
+    protected setHTMLFontSize(): void;
+    clearCanvas(): void;
+    /**
+     * 设备像素比
+     * window 环境下自动获取, 其余环境手动传入
+     */
+    protected setDpr(): void;
+    /**
+     * 重置盒子和canvas的宽高
+     */
+    private resetWidthAndHeight;
+    /**
+     * 根据 dpr 缩放 canvas 并处理位移
+     */
+    protected zoomCanvas(): void;
+    /**
+     * 从 window 对象上获取一些方法
+     */
+    private initWindowFunction;
+    isWeb(): boolean;
+    /**
+     * 异步加载图片并返回图片的几何信息
+     * @param src 图片路径
+     * @param info 图片信息
+     */
+    protected loadImg(src: string, info: ImgItemType, resolveName?: string): Promise<ImgType>;
+    /**
+     * 公共绘制图片的方法
+     * @param imgObj 图片对象
+     * @param rectInfo: [x轴位置, y轴位置, 渲染宽度, 渲染高度]
+     */
+    protected drawImage(ctx: CanvasRenderingContext2D, imgObj: ImgType, ...rectInfo: [...Tuple<number, 4>, ...Partial<Tuple<number, 4>>]): void;
+    /**
+     * 计算图片的渲染宽高
+     * @param imgObj 图片标签元素
+     * @param imgInfo 图片信息
+     * @param maxWidth 最大宽度
+     * @param maxHeight 最大高度
+     * @return [渲染宽度, 渲染高度]
+     */
+    protected computedWidthAndHeight(imgObj: ImgType, imgInfo: ImgItemType, maxWidth: number, maxHeight: number): [number, number];
+    /**
+     * 转换单位
+     * @param { string } value 将要转换的值
+     * @param { number } denominator 分子
+     * @return { number } 返回新的字符串
+     */
+    protected changeUnits(value: string, denominator?: number): number;
+    /**
+     * 获取长度
+     * @param length 将要转换的长度
+     * @param maxLength 最大长度
+     * @return 返回长度
+     */
+    protected getLength(length: string | number | undefined, maxLength?: number): number;
+    /**
+     * 获取相对(居中)X坐标
+     * @param width
+     * @param col
+     */
+    protected getOffsetX(width: number, maxWidth?: number): number;
+    protected getOffscreenCanvas(width: number, height: number): {
+        _offscreenCanvas: HTMLCanvasElement;
+        _ctx: CanvasRenderingContext2D;
+    } | void;
+    /**
+     * 添加一个新的响应式数据 (临时)
+     * @param data 数据
+     * @param key 属性
+     * @param value 新值
+     */
+    $set(data: object, key: string | number, value: any): void;
+    /**
+     * 添加一个属性计算 (临时)
+     * @param data 源数据
+     * @param key 属性名
+     * @param callback 回调函数
+     */
+    protected $computed(data: object, key: string, callback: Function): void;
+    /**
+     * 添加一个观察者 create user watcher
+     * @param expr 表达式
+     * @param handler 回调函数
+     * @param watchOpt 配置参数
+     * @return 卸载当前观察者的函数 (暂未返回)
+     */
+    protected $watch(expr: string | Function, handler: Function | WatchOptType, watchOpt?: WatchOptType): Function;
+}
+
+declare type PrizeFontType$2 = FontItemType & FontExtendType;
+declare type ButtonFontType$1 = FontItemType & {};
+declare type BlockImgType$2 = ImgItemType & {
+    rotate?: boolean;
+};
+declare type PrizeImgType$2 = ImgItemType & {};
+declare type ButtonImgType$1 = ImgItemType & {};
+declare type BlockType$2 = {
+    padding?: string;
+    background?: BackgroundType;
+    imgs?: Array<BlockImgType$2>;
+};
+declare type PrizeType$2 = {
+    range?: number;
+    background?: BackgroundType;
+    fonts?: Array<PrizeFontType$2>;
+    imgs?: Array<PrizeImgType$2>;
+};
+declare type ButtonType$1 = {
+    radius?: string;
+    pointer?: boolean;
+    background?: BackgroundType;
+    fonts?: Array<ButtonFontType$1>;
+    imgs?: Array<ButtonImgType$1>;
+};
+declare type DefaultConfigType$2 = {
+    gutter?: string | number;
+    offsetDegree?: number;
+    speed?: number;
+    speedFunction?: string;
+    accelerationTime?: number;
+    decelerationTime?: number;
+    stopRange?: number;
+};
+declare type DefaultStyleType$2 = {
+    background?: BackgroundType;
+    fontColor?: PrizeFontType$2['fontColor'];
+    fontSize?: PrizeFontType$2['fontSize'];
+    fontStyle?: PrizeFontType$2['fontStyle'];
+    fontWeight?: PrizeFontType$2['fontWeight'];
+    lineHeight?: PrizeFontType$2['lineHeight'];
+    wordWrap?: PrizeFontType$2['wordWrap'];
+    lengthLimit?: PrizeFontType$2['lengthLimit'];
+    lineClamp?: PrizeFontType$2['lineClamp'];
+};
+declare type StartCallbackType$1 = (e: MouseEvent) => void;
+declare type EndCallbackType$2 = (prize: object) => void;
+interface LuckyWheelConfig {
+    width: string | number;
+    height: string | number;
+    blocks?: Array<BlockType$2>;
+    prizes?: Array<PrizeType$2>;
+    buttons?: Array<ButtonType$1>;
+    defaultConfig?: DefaultConfigType$2;
+    defaultStyle?: DefaultStyleType$2;
+    start?: StartCallbackType$1;
+    end?: EndCallbackType$2;
+}
+
+declare class LuckyWheel extends Lucky {
+    private blocks;
+    private prizes;
+    private buttons;
+    private defaultConfig;
+    private defaultStyle;
+    private _defaultConfig;
+    private _defaultStyle;
+    private startCallback?;
+    private endCallback?;
+    private Radius;
+    private prizeRadius;
+    private prizeDeg;
+    private prizeAng;
+    private rotateDeg;
+    private maxBtnRadius;
+    private startTime;
+    private endTime;
+    private stopDeg;
+    private endDeg;
+    private FPS;
+    /**
+     * 游戏当前的阶段
+     * step = 0 时, 游戏尚未开始
+     * step = 1 时, 此时处于加速阶段
+     * step = 2 时, 此时处于匀速阶段
+     * step = 3 时, 此时处于减速阶段
+     */
+    private step;
+    /**
+     * 中奖索引
+     * prizeFlag = undefined 时, 处于开始抽奖阶段, 正常旋转
+     * prizeFlag >= 0 时, 说明stop方法被调用, 并且传入了中奖索引
+     * prizeFlag === -1 时, 说明stop方法被调用, 并且传入了负值, 本次抽奖无效
+     */
+    private prizeFlag;
+    private ImageCache;
+    /**
+     * 大转盘构造器
+     * @param config 配置项
+     * @param data 抽奖数据
+     */
+    constructor(config: UserConfigType, data: LuckyWheelConfig);
+    protected resize(): void;
+    protected initLucky(): void;
+    /**
+     * 初始化数据
+     * @param data
+     */
+    private initData;
+    /**
+     * 初始化属性计算
+     */
+    private initComputed;
+    /**
+     * 初始化观察者
+     */
+    private initWatch;
+    /**
+     * 初始化 canvas 抽奖
+     */
+    init(): Promise<void>;
+    private initImageCache;
+    /**
+     * canvas点击事件
+     * @param e 事件参数
+     */
+    protected handleClick(e: MouseEvent): void;
+    /**
+     * 根据索引单独加载指定图片并缓存
+     * @param cellName 模块名称
+     * @param cellIndex 模块索引
+     * @param imgName 模块对应的图片缓存
+     * @param imgIndex 图片索引
+     */
+    private loadAndCacheImg;
+    private drawBlock;
+    /**
+     * 开始绘制
+     */
+    protected draw(): void;
+    /**
+     * 刻舟求剑
+     */
+    private carveOnGunwaleOfAMovingBoat;
+    /**
+     * 对外暴露: 开始抽奖方法
+     */
+    play(): void;
+    /**
+     * 对外暴露: 缓慢停止方法
+     * @param index 中奖索引
+     */
+    stop(index?: number): void;
+    /**
+     * 实际开始执行方法
+     * @param num 记录帧动画执行多少次
+     */
+    private run;
+    /**
+     * 换算渲染坐标
+     * @param x
+     * @param y
+     */
+    protected conversionAxis(x: number, y: number): [number, number];
+}
+
+declare type PrizeFontType$1 = FontItemType & FontExtendType;
+declare type ButtonFontType = FontItemType & FontExtendType;
+declare type BlockImgType$1 = ImgItemType & {};
+declare type PrizeImgType$1 = ImgItemType & {
+    activeSrc?: string;
+};
+declare type ButtonImgType = ImgItemType & {};
+declare type BlockType$1 = {
+    borderRadius?: BorderRadiusType;
+    background?: BackgroundType;
+    padding?: string;
+    paddingTop?: string | number;
+    paddingRight?: string | number;
+    paddingBottom?: string | number;
+    paddingLeft?: string | number;
+    imgs?: Array<BlockImgType$1>;
+};
+declare type CellType<T, U> = {
+    x: number;
+    y: number;
+    col?: number;
+    row?: number;
+    borderRadius?: BorderRadiusType;
+    background?: BackgroundType;
+    shadow?: ShadowType;
+    fonts?: Array<T>;
+    imgs?: Array<U>;
+};
+declare type PrizeType$1 = CellType<PrizeFontType$1, PrizeImgType$1> & {
+    range?: number;
+    disabled?: boolean;
+};
+declare type ButtonType = CellType<ButtonFontType, ButtonImgType> & {
+    callback?: Function;
+};
+declare type DefaultConfigType$1 = {
+    gutter?: number;
+    speed?: number;
+    accelerationTime?: number;
+    decelerationTime?: number;
+};
+declare type DefaultStyleType$1 = {
+    borderRadius?: BorderRadiusType;
+    background?: BackgroundType;
+    shadow?: ShadowType;
+    fontColor?: PrizeFontType$1['fontColor'];
+    fontSize?: PrizeFontType$1['fontSize'];
+    fontStyle?: PrizeFontType$1['fontStyle'];
+    fontWeight?: PrizeFontType$1['fontWeight'];
+    lineHeight?: PrizeFontType$1['lineHeight'];
+    wordWrap?: PrizeFontType$1['wordWrap'];
+    lengthLimit?: PrizeFontType$1['lengthLimit'];
+    lineClamp?: PrizeFontType$1['lineClamp'];
+};
+declare type ActiveStyleType = {
+    background?: BackgroundType;
+    shadow?: ShadowType;
+    fontColor?: PrizeFontType$1['fontColor'];
+    fontSize?: PrizeFontType$1['fontSize'];
+    fontStyle?: PrizeFontType$1['fontStyle'];
+    fontWeight?: PrizeFontType$1['fontWeight'];
+    lineHeight?: PrizeFontType$1['lineHeight'];
+};
+declare type RowsType = number;
+declare type ColsType = number;
+declare type StartCallbackType = (e: MouseEvent, button?: ButtonType) => void;
+declare type EndCallbackType$1 = (prize: object) => void;
+interface LuckyGridConfig {
+    width: string | number;
+    height: string | number;
+    rows?: RowsType;
+    cols?: ColsType;
+    blocks?: Array<BlockType$1>;
+    prizes?: Array<PrizeType$1>;
+    buttons?: Array<ButtonType>;
+    button?: ButtonType;
+    defaultConfig?: DefaultConfigType$1;
+    defaultStyle?: DefaultStyleType$1;
+    activeStyle?: ActiveStyleType;
+    start?: StartCallbackType;
+    end?: EndCallbackType$1;
+}
+
+declare class LuckyGrid extends Lucky {
+    private rows;
+    private cols;
+    private blocks;
+    private prizes;
+    private buttons;
+    private button?;
+    private defaultConfig;
+    private defaultStyle;
+    private activeStyle;
+    private _defaultConfig;
+    private _defaultStyle;
+    private _activeStyle;
+    private startCallback?;
+    private endCallback?;
+    private cellWidth;
+    private cellHeight;
+    private startTime;
+    private endTime;
+    private currIndex;
+    private stopIndex;
+    private endIndex;
+    private demo;
+    private timer;
+    private FPS;
+    /**
+     * 游戏当前的阶段
+     * step = 0 时, 游戏尚未开始
+     * step = 1 时, 此时处于加速阶段
+     * step = 2 时, 此时处于匀速阶段
+     * step = 3 时, 此时处于减速阶段
+     */
+    private step;
+    /**
+     * 中奖索引
+     * prizeFlag = undefined 时, 处于开始抽奖阶段, 正常旋转
+     * prizeFlag >= 0 时, 说明stop方法被调用, 并且传入了中奖索引
+     * prizeFlag === -1 时, 说明stop方法被调用, 并且传入了负值, 本次抽奖无效
+     */
+    private prizeFlag;
+    private cells;
+    private prizeArea;
+    private ImageCache;
+    /**
+     * 九宫格构造器
+     * @param config 配置项
+     * @param data 抽奖数据
+     */
+    constructor(config: UserConfigType, data: LuckyGridConfig);
+    protected resize(): void;
+    protected initLucky(): void;
+    /**
+     * 初始化数据
+     * @param data
+     */
+    private initData;
+    /**
+     * 初始化属性计算
+     */
+    private initComputed;
+    /**
+     * 初始化观察者
+     */
+    private initWatch;
+    /**
+     * 初始化 canvas 抽奖
+     */
+    init(): Promise<void>;
+    private initImageCache;
+    /**
+     * canvas点击事件
+     * @param e 事件参数
+     */
+    protected handleClick(e: MouseEvent): void;
+    /**
+     * 根据索引单独加载指定图片并缓存
+     * @param cellName 模块名称
+     * @param cellIndex 模块索引
+     * @param imgName 模块对应的图片缓存
+     * @param imgIndex 图片索引
+     */
+    private loadAndCacheImg;
+    /**
+     * 绘制九宫格抽奖
+     */
+    protected draw(): void;
+    /**
+     * 处理背景色
+     * @param x
+     * @param y
+     * @param width
+     * @param height
+     * @param background
+     * @param isActive
+     */
+    private handleBackground;
+    /**
+     * 刻舟求剑
+     */
+    private carveOnGunwaleOfAMovingBoat;
+    /**
+     * 对外暴露: 开始抽奖方法
+     */
+    play(): void;
+    /**
+     * 对外暴露: 缓慢停止方法
+     * @param index 中奖索引
+     */
+    stop(index?: number): void;
+    /**
+     * 实际开始执行方法
+     * @param num 记录帧动画执行多少次
+     */
+    private run;
+    /**
+     * 计算奖品格子的几何属性
+     * @param { array } [...矩阵坐标, col, row]
+     * @return { array } [...真实坐标, width, height]
+     */
+    private getGeometricProperty;
+    /**
+     * 换算渲染坐标
+     * @param x
+     * @param y
+     */
+    protected conversionAxis(x: number, y: number): [number, number];
+}
+
+declare type PrizeFontType = FontItemType & FontExtendType;
+declare type BlockImgType = ImgItemType & {};
+declare type PrizeImgType = ImgItemType;
+declare type BlockType = {
+    borderRadius?: BorderRadiusType;
+    background?: BackgroundType;
+    padding?: string;
+    paddingTop?: string | number;
+    paddingRight?: string | number;
+    paddingBottom?: string | number;
+    paddingLeft?: string | number;
+    imgs?: Array<BlockImgType>;
+};
+declare type PrizeType = {
+    borderRadius?: BorderRadiusType;
+    background?: BackgroundType;
+    fonts?: Array<PrizeFontType>;
+    imgs?: Array<PrizeImgType>;
+};
+declare type SlotType = {
+    order?: number[];
+    speed?: number;
+    direction?: 1 | -1;
+};
+declare type DefaultConfigType = {
+    /**
+     * vertical 为纵向旋转
+     * horizontal 为横向旋转
+     */
+    mode?: 'vertical' | 'horizontal';
+    /**
+     * 当排列方向 = `vertical`时
+     *    1 bottom to top
+     *   -1 top to bottom
+     * 当排列方向 = `horizontal`时
+     *    1 right to left
+     *   -1 left to right
+     */
+    direction?: 1 | -1;
+    rowSpacing?: number;
+    colSpacing?: number;
+    speed?: number;
+    accelerationTime?: number;
+    decelerationTime?: number;
+};
+declare type DefaultStyleType = {
+    borderRadius?: BorderRadiusType;
+    background?: BackgroundType;
+    fontColor?: PrizeFontType['fontColor'];
+    fontSize?: PrizeFontType['fontSize'];
+    fontStyle?: PrizeFontType['fontStyle'];
+    fontWeight?: PrizeFontType['fontWeight'];
+    lineHeight?: PrizeFontType['lineHeight'];
+    wordWrap?: PrizeFontType['wordWrap'];
+    lengthLimit?: PrizeFontType['lengthLimit'];
+    lineClamp?: PrizeFontType['lineClamp'];
+};
+declare type EndCallbackType = (prize: PrizeType | undefined) => void;
+interface SlotMachineConfig {
+    width: string | number;
+    height: string | number;
+    blocks?: Array<BlockType>;
+    prizes?: Array<PrizeType>;
+    slots?: Array<SlotType>;
+    defaultConfig?: DefaultConfigType;
+    defaultStyle?: DefaultStyleType;
+    end?: EndCallbackType;
+}
+
+declare class SlotMachine extends Lucky {
+    private blocks;
+    private prizes;
+    private slots;
+    private defaultConfig;
+    private _defaultConfig;
+    private defaultStyle;
+    private _defaultStyle;
+    private endCallback;
+    private _offscreenCanvas?;
+    private cellWidth;
+    private cellHeight;
+    private cellAndSpacing;
+    private widthAndSpacing;
+    private heightAndSpacing;
+    private FPS;
+    private scroll;
+    private stopScroll;
+    private endScroll;
+    private startTime;
+    private endTime;
+    /**
+     * 游戏当前的阶段
+     * step = 0 时, 游戏尚未开始
+     * step = 1 时, 此时处于加速阶段
+     * step = 2 时, 此时处于匀速阶段
+     * step = 3 时, 此时处于减速阶段
+     */
+    private step;
+    /**
+     * 中奖索引
+     * prizeFlag = undefined 时, 处于开始抽奖阶段, 正常旋转
+     * prizeFlag >= 0 时, 说明stop方法被调用, 并且传入了中奖索引
+     * prizeFlag === -1 时, 说明stop方法被调用, 并且传入了负值, 本次抽奖无效
+     */
+    private prizeFlag;
+    private prizeArea?;
+    private ImageCache;
+    /**
+     * 老虎机构造器
+     * @param config 配置项
+     * @param data 抽奖数据
+     */
+    constructor(config: UserConfigType, data: SlotMachineConfig);
+    protected resize(): void;
+    protected initLucky(): void;
+    /**
+     * 初始化数据
+     * @param data
+     */
+    private initData;
+    /**
+     * 初始化属性计算
+     */
+    private initComputed;
+    /**
+     * 初始化观察者
+     */
+    private initWatch;
+    /**
+     * 初始化 canvas 抽奖
+     */
+    init(): Promise<void>;
+    private initImageCache;
+    /**
+     * 根据索引单独加载指定图片并缓存
+     * @param cellName 模块名称
+     * @param cellIndex 模块索引
+     * @param imgName 模块对应的图片缓存
+     * @param imgIndex 图片索引
+     */
+    private loadAndCacheImg;
+    /**
+     * 绘制离屏canvas
+     */
+    protected drawOffscreenCanvas(): void;
+    /**
+     * 绘制背景区域
+     */
+    protected drawBlocks(): SlotMachine['prizeArea'];
+    /**
+     * 绘制老虎机抽奖
+     */
+    protected draw(): void;
+    /**
+     * 刻舟求剑
+     */
+    private carveOnGunwaleOfAMovingBoat;
+    /**
+     * 对外暴露: 开始抽奖方法
+     */
+    play(): void;
+    stop(index: number | number[]): void;
+    /**
+     * 让游戏动起来
+     * @param num 记录帧动画执行多少次
+     */
+    private run;
+    private displacement;
+    private displacementWidthOrHeight;
+}
+
+/**
+ * 切割圆角
+ * @param img 将要裁剪的图片对象
+ * @param radius 裁剪的圆角半径
+ * @returns 返回一个离屏 canvas 用于渲染
+ */
+declare const cutRound: (img: ImgType, radius: number) => ImgType;
+/**
+ * 透明度
+ * @param img 将要处理的图片对象
+ * @param opacity 透明度
+ * @returns 返回一个离屏 canvas 用于渲染
+ */
+declare const opacity: (img: ImgType, opacity: number) => ImgType;
+
+export { LuckyGrid, LuckyWheel, SlotMachine, cutRound, opacity };

+ 143 - 0
components/zs-luck-dialog/index.vue

@@ -0,0 +1,143 @@
+<template>
+	<view class="modal">
+	
+		<view class="dialog-border">
+			<image class="top-icon"
+				src="https://hyxhsh.oss-cn-chengdu.aliyuncs.com/63b7c68b71a69169d1b33f92/store/bdb/user/avatar/aDrawbTdSWjg1742dea71dd1c645b6b398a619bfc9fe.png/1.png"
+				mode=""></image>
+			<view class="dialog-content">
+				<image class="title-icon"
+					:src="titleIcon"
+					mode=""></image>
+					
+					<slot></slot>
+			</view>
+			<view class="btn-box">
+				<view class="btn" @click="submit">
+					{{btnText}}
+				</view>
+			</view>
+			<image class="close" src="@/static/close.png" mode="" @click="handleClose('show')"></image>
+		</view>
+	</view>
+	
+	
+</template>
+
+<script>
+	export default {
+		props: {
+			titleIcon: {
+				type: String,
+				default: 'https://hyxhsh.oss-cn-chengdu.aliyuncs.com/63b7c68b71a69169d1b33f92/store/bdb/user/avatar/ApX9kwEUYgE84e54016e951eaab20c5a504e8c5df6f2.png/1.png'
+			},
+			btnText: {
+				type: String,
+				default: '确认'
+			},
+		},
+		data() {
+			return {
+				
+			}
+		},
+		methods: {
+			submit(){
+				this.$emit('submit')
+			},
+			handleClose() {
+				this.$emit('close')
+			}
+		},
+		
+	}
+</script>
+
+<style lang="scss" scoped>
+	
+	.modal {
+		position: fixed;
+		top: 0%;
+		left: 0;
+		width: 100%;
+		height: 100%;
+		display: flex;
+		justify-content: center;
+		align-items: center;
+		background: rgba(0, 0, 0, .4);
+	
+	}
+	
+	.dialog-border {
+		width: 590rpx;
+		height: 672rpx;
+		background: #FF5642;
+		box-shadow: inset 0rpx 6rpx 12rpx 2rpx #FFFFFF;
+		border-radius: 24rpx 24rpx 24rpx 24rpx;
+		border: 1rpx solid #FFFFFF;
+		display: flex;
+		justify-content: center;
+		align-items: center;
+		position: relative;
+		.close{
+			width: 40rpx;
+			height: 40rpx;
+			position: absolute;
+			bottom: -70rpx;
+			left: calc(50% - 20rpx);
+		}
+	
+		.top-icon {
+			width: 332rpx;
+			height: 142rpx;
+			position: absolute;
+			top: -130rpx;
+			left: calc(50% - 166rpx);
+			vertical-align: bottom;
+		}
+	
+		.btn-box {
+			position: absolute;
+			left: 0%;
+			bottom: 0%;
+			z-index: 2;
+			width: 590rpx;
+			height: 134rpx;
+			background: linear-gradient(180deg, #FFF3E2 0%, #F4AF9E 100%);
+			border-radius: 0rpx 0rpx 24rpx 24rpx;
+			display: flex;
+			align-items: center;
+			justify-content: center;
+	
+			.btn {
+				width: 258rpx;
+				height: 80rpx;
+				line-height: 80rpx;
+				text-align: center;
+				background: #FF4D3A;
+				box-shadow: 0rpx 6rpx 12rpx 2rpx #FF4D3A, inset 0rpx 16rpx 4rpx 2rpx #E8422F;
+				border-radius: 72rpx 72rpx 72rpx 72rpx;
+				border: 2rpx solid #FFFFFF;
+				font-weight: bold;
+				font-size: 32rpx;
+				color: #FFFFFF;
+	
+			}
+		}
+	
+		.dialog-content {
+			width: 542rpx;
+			height: 612rpx;
+			overflow: auto;
+			background: linear-gradient(151deg, #FFFFFF 0%, #FFFFFF 0%, #FFD9BF 100%);
+			border-radius: 24rpx 24rpx 24rpx 24rpx;
+	
+			.title-icon {
+				width: 430rpx;
+				height: 84rpx;
+				margin-left: 84rpx;
+				margin-top: 30rpx;
+			}
+		}
+	}
+</style>

+ 3 - 0
detail/discountsDetail/index.vue

@@ -222,6 +222,9 @@
 					if(!obj.range){
 						delete obj.range
 					}
+					if(uni.getStorageSync('city').indexOf(uni.getStorageSync('HomeCity')) == -1){
+						obj.city = uni.getStorageSync('HomeCity')
+					}
 					search(obj).then(res => {
 						if (res.state == 'Success') {
 							this.list = this.list.concat(res.content.records)

+ 3 - 2
detail/shopDetail/shopDetail.vue

@@ -8,14 +8,14 @@
 				<view class="info-box">
 					<view class="title-box">
 						<view class="title">
-							{{info.shopName}}
+							{{info.shopName ||''}}
 						</view>
 						<image class="tel" src="../../static/phone.png" mode="" v-if="info.serviceTel" @click="handleCall"></image>
 					</view>
 					<view class="address-box" @click="handleAdress">
 						<view class="distance">
 							<image class="address-icon" src="../../static/address-icon.png" mode=""></image>
-							{{info.address}}
+							{{info.address || ''}}
 						<!-- <image class="address-icon" src="../../static/address-icon.png" mode=""></image>	距你直线距离{{(info.shopVo.distance/1000).toFixed(2)}}km &nbsp;·&nbsp;{{info.district}} -->
 						</view>
 					</view>
@@ -318,6 +318,7 @@
 						this.info.mapLon = res.content.mapLon
 						this.info.detailContent = res.content.detailContent
 						this.info.shopName = res.content.shopName
+						this.info.logoPath = res.content.logoPath
 						uni.setNavigationBarTitle({
 							title:res.content.shopName
 						})

+ 88 - 11
detail/virtualGoods/detail.vue

@@ -3,17 +3,17 @@
 		<view class="content">
 			<!-- <view class="popup-title">会员充值</view> -->
 			<view class="goods-box">
-				<image class="icon" :src="chooseInfo.mainImg" mode=""></image>
+				<image class="icon" :src="chooseInfo.cover.length&&chooseInfo.cover[0]" mode=""></image>
 				<view class="goods-info">
 					<view class="goods-title">
-						{{chooseInfo.item_name}}
+						{{chooseInfo.title}}
 					</view>
 					<view class="goods-desc">
-						{{chooseInfo.catalogName}}
+						{{chooseInfo.title}}
 					</view>
 				</view>
 				<view class="price">
-					¥{{Number(chooseInfo.channel_price).toFixed(2)}}
+					¥{{chooseInfo.salePrice}}
 				</view>
 			</view>
 
@@ -24,7 +24,7 @@
 			<u--form labelPosition="top" :model="form" ref="uForm" borderBottom labelWidth="180rpx" :labelStyle="{'color': '#222222',
 		'font-weight': 'bold','line-height':'60rpx','font-size': '28rpx'}">
 				<u-form-item label="账号类型" borderBottom ref="item1">
-					<view class="type-box" @click="show = true">
+					<view class="type-box" @click="open">
 						<view class="">
 							{{type}}
 						</view>
@@ -39,6 +39,42 @@
 
 				<rich-text :nodes="chooseInfo.goodsDescribe"></rich-text>
 			</view>
+			
+			<view class="sub-title">
+				商品说明
+			</view>
+			<view class="notice-text">
+				<view class="">
+					充值方式:自动充值,填写登录时绑定的手机号/邮箱号等对应账号;
+				</view>
+				<view class="">
+					有效期说明:有效期自购买之日算起,过期即失效。
+				</view>
+			</view>
+			
+			<view class="sub-title">
+				温馨提示
+			</view>
+			<view class="notice-text">
+				<view class="">
+					1、仅支持在中国境内使用。
+				</view>
+				<view class="">
+					2、本产品为虚拟商品,一经付款,无法办理退款退货;
+				</view>
+				<view class="">
+					3、充值前请核对充值账号,切勿填错,如因用户个人原因充错,责任自行承担;
+				</view>
+				<view class="">
+					4、充值成功后,请登录官方网站/APP查询您的会员状态和有效期;
+				</view>
+				<view class="">
+					5、如有其他限制,以所充值会员的官方规定为准。
+				</view>
+			</view>
+			
+			
+			
 			<button class="btn" type="default" :loading="loading" @click="handleBuy">确认</button>
 		</view>
 		
@@ -52,10 +88,10 @@
 		4、充值成功后,请登录官方网站/APP查询您的会员状态和有效期;
 		5、如有其他限制,以所充值会员的官方规定为准。 -->
 		
-		<u-modal :show="iosShow" content='暂不提供IOS用户进行充值业务' @confirm="iosShow = false"></u-modal>
+		<u-modal :show="iosShow" content='暂不提供IOS用户进行充值业务' @confirm="confirm"></u-modal>
 		
 		<!-- 选择账号类型 -->
-		<u-picker :show="show" :value='shopIndex' keyName="label" :closeOnClickOverlay="false" :columns="list" @confirm="choose" @cancel="show = false"></u-picker>
+		<u-picker :show="show" :value='shopIndex' keyName="label" :closeOnClickOverlay="false" :columns="list" @confirm="choose" @cancel="cancel"></u-picker>
 		
 	</view>
 </template>
@@ -85,7 +121,7 @@
 							value:2,
 						},
 						{
-							label:'其他',
+							label:'邮箱',
 							value:0,
 						},
 					]
@@ -93,6 +129,15 @@
 			}
 		},
 		methods: {
+			confirm(){
+				this.iosShow = false
+			},
+			cancel(){
+				this.show = false
+			},
+			open(){
+				this.show = true
+			},
 			choose(val){
 				console.log(val.value[0]);
 				this.type = val.value[0].label
@@ -117,12 +162,31 @@
 									that.show = false
 									that.iosShow = true
 								}else{
+									if(that.form.accountType == 1 &&!uni.$u.test.mobile(that.form.account)){
+										return uni.showToast({
+											title:'账号格式错误',
+											icon:'none'
+										})
+									}else if(that.form.accountType == 2 && !uni.$u.test.digits(that.form.account)){
+										return uni.showToast({
+											title:'账号格式错误',
+											icon:'none'
+										})
+									}else if(that.form.accountType == 0 &&!uni.$u.test.email(that.form.account)){
+										return uni.showToast({
+											title:'账号格式错误',
+											icon:'none'
+										})
+									}
+									
 									let info = {
-										price:Number(that.chooseInfo.channel_price).toFixed(2),
-										goodsName:that.chooseInfo.item_name,
+										price:that.chooseInfo.salePrice,
+										goodsName:that.chooseInfo.title,
 										accountType:that.form.accountType,
 										rechargeAccount:that.form.account,
-										productId:that.chooseInfo.product_id
+										productId:that.chooseInfo.pid,
+										goodsImg:that.chooseInfo.cover&&that.chooseInfo.cover.length?that.chooseInfo.cover[0]:'',
+										
 									}
 									uni.navigateTo({
 										url:`/detail/virtualGoods/pay`,
@@ -173,6 +237,7 @@
 			eventChannel.on('data', function(data) {
 				console.log('da',data);
 				that.chooseInfo = data
+				that.chooseInfo.salePrice = (that.chooseInfo.salePrice/100).toFixed(2)
 				
 			})
 		}
@@ -181,6 +246,7 @@
 
 <style lang="scss" scoped>
 	.virtualGoods-detail {
+		padding-bottom: 200rpx;
 		.content{
 			padding: 24rpx;
 			.popup-title{
@@ -242,6 +308,17 @@
 				padding-top: 28rpx;
 				overflow: auto;
 			}
+			.sub-title{
+				font-weight: bold;
+				font-size: 32rpx;
+				color: #222222;
+				margin: 20rpx 0;
+			}
+			.notice-text{
+				font-size: 24rpx;
+				color: #AAAAAA;
+				line-height: 44rpx;
+			}
 			.btn{
 				position: fixed;
 				left: 30rpx;

+ 43 - 31
detail/virtualGoods/index.vue

@@ -1,23 +1,24 @@
 <template>
 	<view class="virtual">
 		
-		<view class="box" v-for="(item,index) in goodsData" :key="index">
-			<view class="goods-item" v-for="i in item.goods" :key="i.mainId" @click="handleItem(i,item.name)">
-			<!-- <view class="goods-item" v-for="(item,index) in goodsData" :key="index" @click="handleItem(item,item.name)"> -->
-				<image class="icon" :src="i.mainImg" mode=""></image>
+		<view class="box">
+		<!-- <view class="box" v-for="(item,index) in goodsData" :key="index"> -->
+			<!-- <view class="goods-item" v-for="i in item.goods" :key="i.mainId" @click="handleItem(i,item.name)"> -->
+			<view class="goods-item" v-for="(item,index) in goodsData" :key="index" @click="handleItem(item)">
+				<image class="icon" :src="item.cover&&item.cover.length&&item.cover[0]" mode=""></image>
 				<view class="goods-info">
-					<view class="goods-title">
-						{{i.name}}
-						<!-- {{item.item_name}} -->
+					<view class="goods-title" >
+						<!-- {{i.name}} -->
+						{{item.title}}
 					</view>
 					<view class="goods-desc">
-						{{item.name}}
-						<!-- {{item.item_name}} -->
+						<!-- {{item.name}} -->
+						{{item.title}}
 					</view>
 				</view>
 				<view class="price">
-					¥{{i.calcPrice}}
-					<!-- ¥{{Number(item.channel_price).toFixed(2)}} -->
+					<!-- ¥{{i.calcPrice}} -->
+					¥{{Number(item.salePrice/100).toFixed(2)}}
 				</view>
 			</view>
 		</view>
@@ -69,7 +70,7 @@
 </template>
 
 <script>
-	import {getVirtuallist,getVirtualInfo} from '@/api/goods.js'
+	import {getVirtuallist,getVirtualInfo,getGoodsList} from '@/api/goods.js'
 	import {SHOP_ID} from '@/utils/config.js'
 	export default {
 		data() {
@@ -83,27 +84,33 @@
 				form:{
 					account:''
 				},
+				
+				query:{
+					regionCode:uni.getStorageSync('regionCode'),
+					productType:['Recharge'],
+					page:0,
+					size:99
+				}
 			}
 		},
 		methods: {
 			handleItem(item,catalogName) {
-				console.log(item,catalogName);
-				this.chooseInfo = JSON.parse(JSON.stringify(item) ) 
-				this.chooseInfo.catalogName = catalogName
-				this.show = true
-				this.getVirtualInfo(item.mainId)
-				
 				// console.log(item,catalogName);
 				// this.chooseInfo = JSON.parse(JSON.stringify(item) ) 
-				// this.chooseInfo.catalogName = catalogName ||'无'
-				// let that = this
-				// uni.navigateTo({
-				// 	url:`/detail/virtualGoods/detail`,
-				// 	  success: function(res) {
-				// 		// 通过eventChannel向被打开页面传送数据
-				// 		res.eventChannel.emit('data', that.chooseInfo)
-				// 	  }
-				// })
+				// this.chooseInfo.catalogName = catalogName
+				// this.show = true
+				// this.getVirtualInfo(item.mainId)
+				console.log(item,catalogName);
+				this.chooseInfo = JSON.parse(JSON.stringify(item) ) 
+				this.chooseInfo.catalogName = catalogName ||'无'
+				let that = this
+				uni.navigateTo({
+					url:`/detail/virtualGoods/detail`,
+					  success: function(res) {
+						// 通过eventChannel向被打开页面传送数据
+						res.eventChannel.emit('data', that.chooseInfo)
+					  }
+				})
 			},
 			 open() {
 			},
@@ -114,13 +121,19 @@
 				uni.showLoading({
 					title:'加载中'
 				})
-				getVirtuallist().then(res=>{
+				getGoodsList(this.query).then(res=>{
 					uni.hideLoading()
 					if(res.state == 'Success'){
-						// this.goodsData = res.content.products
-						this.goodsData = res.content
+						this.goodsData = this.goodsData.concat(res.content.content)
 					}
 				})
+				// getVirtuallist().then(res=>{
+				// 	uni.hideLoading()
+				// 	if(res.state == 'Success'){
+				// 		this.goodsData = res.content.products
+				// 		// this.goodsData = res.content
+				// 	}
+				// })
 			},
 			getVirtualInfo(commodityId){
 				this.loadingPrice = true
@@ -196,7 +209,6 @@
 						}
 					})
 				}
-				
 			
 			}
 			

+ 7 - 15
detail/virtualGoods/pay.vue

@@ -1,18 +1,9 @@
 
 
 
-<template>
+<!-- <template>
 	<view class="pay">
 
-		<!-- <view class="shop-info" v-if="!isVisual">
-			<view class="shop-name">
-				{{info.shopName}}
-			</view>
-			<view class="address">
-				{{info.address}}
-			</view>
-		</view> -->
-
 		<view class="pay-info">
 			<view class="goods-info">
 				<image class="goods-img" :src="info.goodsImg" mode=""></image>
@@ -478,6 +469,7 @@
 		}
 	}
 </style>
+ -->
 
 
 
@@ -487,8 +479,7 @@
 
 
 
-
-<!-- <template>
+<template>
 	<view class="pay">
 
 		<view class="pay-info">
@@ -498,9 +489,9 @@
 					<view class="goods-name">
 						{{info.goodsName}}
 					</view>
-					<view class="num">
+					<!-- <view class="num">
 						{{info.goodsDescribe}}
-					</view>
+					</view> -->
 					<view class="price">
 						¥{{info.price}}
 					</view>
@@ -722,6 +713,7 @@
 				that.createQuery.productId = data.productId
 				that.info.goodsName = data.goodsName
 				that.info.price = data.price
+				that.info.goodsImg = data.goodsImg
 			
 				console.log('data', data);
 			})
@@ -929,4 +921,4 @@
 			}
 		}
 	}
-</style> -->
+</style>

+ 921 - 0
luckyDraw/index.vue

@@ -0,0 +1,921 @@
+<template>
+	<view class="luckyDraw" :style="{backgroundImage:`url(${bg})` }">
+
+		<view class="lucky-box" :class="isHidden?'hidden':''">
+			<LuckyWheel ref="myLucky" width="600rpx" height="600rpx" :blocks="blocks" :prizes="prizes"
+				:buttons="buttons" :defaultStyle="defaultStyle" @start="startCallBack" @end="endCallBack" />
+			<view class="desk">
+				<image class="icon"
+					src="https://hyxhsh.oss-cn-chengdu.aliyuncs.com/63b7c68b71a69169d1b33f92/store/bdb/user/avatar/HmntmDPPKjjS11b74d9dd70452a39648bfab16eb88cb.png/1.png"
+					mode=""></image>
+				<view class="times">
+					抽奖次数:{{prizeNum}}次
+				</view>
+			</view>
+		</view>
+		
+		<view class="rule-btn" @click="handleRule">
+			抽奖规则
+		</view>
+		
+		
+		<view class="box">
+			<view class="title">
+				<image class="icon" src="https://hyxhsh.oss-cn-chengdu.aliyuncs.com/63b7c68b71a69169d1b33f92/store/bdb/user/avatar/K4cEa0zOfcQ6371bdc4794cc3926a80e57995db464c9.png/1.png" mode=""></image>
+				<!-- 中奖记录 -->
+			</view>
+			<view class="content box-container">
+			<u-empty v-if="!logList.length" text="中奖记录空空如也" iconSize="200rpx" textSize="28rpx">
+				</u-empty>
+				<zs-list class="store-box" mt="0" @load="prizeList" :status="status">
+					<view class="item" v-for="(item,index) in logList" :key="index">
+						恭喜 
+						<text class="yellow">
+							{{item.nickname}}
+						</text>
+						获得
+						<text class="yellow">
+							{{item.prizeName}}
+						</text>
+					</view>
+				</zs-list>
+			</view>
+		</view>
+		<view class="box">
+			<view class="title">
+				<image class="icon" src="https://hyxhsh.oss-cn-chengdu.aliyuncs.com/63b7c68b71a69169d1b33f92/store/bdb/user/avatar/NWz4vrvoR3UX83e62d6820324af95a2878ccedd39049.png/1.png" mode=""></image>
+				<!-- 活动奖品 -->
+			</view>
+			<view class="content flex-box">
+				<view class="item" v-for="(item,index) in list" :key="index">
+					<image class="icon" :src="item.prizeImg" mode=""></image>
+					<view class="name">
+						{{item.prizeName}}
+					</view>
+				</view>
+			</view>
+		</view>
+		
+		<view class="box">
+			<view class="title">
+				<image class="icon" src="https://hyxhsh.oss-cn-chengdu.aliyuncs.com/63b7c68b71a69169d1b33f92/store/bdb/user/avatar/YfCL69lDjHXI5b33c3b17b10267f2aa2a41a152aa42f.png/1.png" mode=""></image>
+				<!-- 我的奖品 -->
+			</view>
+			<view class="more-btn" @click="handleMore">
+				查看
+			</view>
+			<view class="content table-box">
+				<view class="header">
+					<view class="num">
+						序号
+					</view>
+					<view class="time">
+						获奖时间
+					</view>
+					<view class="info">
+						奖品信息
+					</view>
+				</view>
+				<view class="table-body">
+			<u-empty v-if="!myLogList.length" text="你还没有中奖记录" iconSize="200rpx" textSize="28rpx"></u-empty>
+					<view class="item" v-for="(item,index) in myLogList" :key="index">
+						<view class="num">
+							{{index+1}}
+						</view>
+						<view class="time">
+							{{item.createTime}}
+						</view>
+						<view class="info">
+							{{item.prizeName}}
+						</view>
+					</view>
+				</view>
+			</view>
+		</view>
+		
+		
+		
+		<!-- 视频会员 -->
+		<zs-luck-dialog v-if="show" btnText="立即充值" @submit="submit" @close="handleClose('show')">
+			<view class="result-box">
+				<view class="goods-name">
+					{{result.prizeName}}
+				</view>
+				<image class="img" :src="result.prizeImg" mode=""></image>
+				
+				<view class="input-box">
+					<view class="label">
+						请输入充值手机号:
+					</view>
+					<input class="input" type="text" :maxlength="11" v-model.trim="account" placeholder="请输入充值账号" />
+				</view>
+				
+				<view class="notice">
+					请立即输入账号进行充值,若不充值则视为放弃
+				</view>
+			</view>
+		</zs-luck-dialog>
+		
+		<!-- 邮寄 -->
+		<zs-luck-dialog v-if="show1" btnText="提交收货信息" @submit="submit1" @close="handleClose('show1')">
+			<view class="result-box">
+				<view class="goods-name">
+					{{result.prizeName}}
+				</view>
+				<image class="img" :src="result.prizeImg" mode=""></image>
+				<view class="sub-title">
+					中奖奖品将邮寄给您,请输入收货信息
+				</view>
+				<u--form :model="noteQuery" ref="uForm" borderBottom labelWidth="180rpx" :labelStyle="{'color': '#222222','font-weight': 'bold','line-height':'60rpx','font-size': '28rpx'}">
+					<u-form-item label="收货人姓名" prop="notes.name" borderBottom ref="item1" required>
+						<u--input v-model.trim="noteQuery.notes.name" placeholder="请输入姓名" border="none"></u--input>
+					</u-form-item>
+					<u-form-item label="收货地址" prop="notes.address" borderBottom ref="item1" required>
+						<u--input v-model.trim="noteQuery.notes.address" :maxlength="50" placeholder="请输入收货地址" border="none"></u--input>
+					</u-form-item>
+					<u-form-item label="联系电话" prop="notes.phone" borderBottom ref="item1" required>
+						<u--input v-model.trim="noteQuery.notes.phone"  :maxlength="11" placeholder="请输入手机号" border="none"></u--input>
+					</u-form-item>
+				</u--form>
+				<view class="notice">
+					说明:为保障商品及时送到您的手中,请填写准确的地
+					址信息,若提交后需要修改地址信息请及时联系客服
+				</view>
+			</view>
+		</zs-luck-dialog>
+		
+		<!-- 线上发货 线下自提 -->
+		<zs-luck-dialog v-if="show2" @submit="handleClose('show2')" @close="handleClose('show2')">
+			<view class="result-box">
+				<view class="goods-name">
+					{{result.prizeName}}
+				</view>
+				<image class="img" :src="result.prizeImg" mode=""></image>
+				<view class="address">
+					{{result.address}}
+				</view>
+				<view class="notice">
+					{{result.sendType == 1?`兑换有效期:请在${result.valid}个工作日内到自提点取货,过期失效`:'优惠劵已存入【我的优惠劵】,可前往【我的优惠劵】中查看使用'}}
+					
+				</view>
+			</view>
+		</zs-luck-dialog>
+		
+		<!-- 未中奖 -->
+		<zs-luck-dialog v-if="show3" :titleIcon="result.prizeType == 5?'https://zswl-dev.oss-cn-chengdu.aliyuncs.com/63b7c68b71a69169d1b33f92/store/bdb/user/avatar/R5gnXtO4tU9I391ccdbfe29a8a21e97108077dbbf4c5.png/1.png':'https://hyxhsh.oss-cn-chengdu.aliyuncs.com/63b7c68b71a69169d1b33f92/store/bdb/user/avatar/ApX9kwEUYgE84e54016e951eaab20c5a504e8c5df6f2.png/1.png'" @submit="handleClose('show3')" @close="handleClose('show3')">
+			<view class="result-box">
+				<view class="goods-name">
+					{{result.prizeName}}
+				</view>
+				<image class="img" :src="result.prizeImg" mode=""></image>
+				<view class="address">
+					{{result.prizeType == 5?'好可惜啊,差一点就中了':result.address}}
+				</view>
+				
+			</view>
+		</zs-luck-dialog>
+		
+		
+		
+		<zs-luck-dialog titleIcon="https://hyxhsh.oss-cn-chengdu.aliyuncs.com/63b7c68b71a69169d1b33f92/store/bdb/user/avatar/DNbGhIUkgQSc756a0dc349b888bddc286ecea867f25f.png/1.png" v-if="show4" @submit="handleClose('show4')" @close="handleClose('show4')">
+			
+			<view class="rule-content">
+				<rich-text class="rule-text" :nodes="rule"></rich-text>
+			</view>
+		</zs-luck-dialog>
+
+	</view>
+</template>
+
+<script>
+	import {
+		prizeList,
+		getLuckyDrawDetail,
+		prize,
+		sendVipPrize,
+		notes,
+		remainder
+	} from "@/api/luckyDraw.js"
+	import LuckyWheel from '@/components/@lucky-canvas/uni/lucky-wheel' // 大转盘
+	export default {
+		components: {
+			LuckyWheel
+		},
+		data() {
+			return {
+				bg:'https://zswl-dev.oss-cn-chengdu.aliyuncs.com/63b7c68b71a69169d1b33f92/store/bdb/user/avatar/rP86PRmpk21z60fd17f0e1363aacf412d0a7f9983abb.png/1.png',
+				isPrize:false,
+				isGet:false,
+				prizeId:0,
+				prizeNum:'',
+				rule:'',
+				prizeIndex:0,
+				result:{
+					prizeImg:'',
+					prizeName:'',
+				},
+				query:{
+					raffleId:0,
+					currentPage:1,
+					pageSize:10,
+				},
+				status:'more',
+				list:[],
+				logList:[],//中奖记录
+				myLogList:[],//我的中奖纪录
+				account:'',//充值账号
+				show: false,
+				show1: false,
+				show2: false,
+				show3: false,
+				show4: false,
+				colorList:['#E2EEF5','#FFF4DC','#FFEAC0'],
+				blocks: [{
+					padding: '60rpx',
+					imgs: [{
+						src: 'https://hyxhsh.oss-cn-chengdu.aliyuncs.com/63b7c68b71a69169d1b33f92/store/bdb/user/avatar/G4CJbO8GK7vJ26e48d74c4cd60260313494a367fbe7b.png/1.png',
+						width: "600rpx",
+						height: '600rpx',
+					}]
+				}],
+				prizes: [],
+				buttons: [{
+						radius: '110rpx',
+						imgs: [{
+							src: 'https://hyxhsh.oss-cn-chengdu.aliyuncs.com/63b7c68b71a69169d1b33f92/store/bdb/user/avatar/eTft9lkLzsdTf25c64b3163deb68039b574142a917bb.png/1.png',
+							top: '-87rpx',
+							width: "110rpx",
+							height: '152rpx',
+						}]
+					},
+				
+				],
+				userId:'',
+				noteQuery:{
+				  "notes": {
+				    "address": "",
+				    "name": "",
+				    "phone": ""
+				  },
+				  "rafflePrizeLogId": 0
+				},
+				rules:{
+					'notes.name': {
+						type: 'string',
+						required: true,
+						message: '请输入姓名',
+						trigger: ['blur', 'change']
+					},
+					'notes.address': {
+						type: 'string',
+						required: true,
+						message: '请输入收货地址',
+						trigger: ['blur', 'change']
+					},
+					'notes.phone': {
+						type: 'string',
+						required: true,
+						message: '请输入手机号',
+						trigger: ['blur', 'change']
+					},
+				}
+				
+			}
+		},
+		computed: {
+			isHidden() {
+				if(this.show||this.show1||this.show2||this.show3||this.show4){
+					return true
+				}else{
+					return false
+				}
+			}
+		},
+		methods: {
+			handleMore(url){
+				uni.navigateTo({
+					url:'/luckyDraw/prizeList?id='+this.query.raffleId
+				})
+			},
+			handleClose(val){
+				this[val] = false
+			},
+			handleRule(){
+				this.show4 = true
+			},
+			// 点击抽奖按钮触发回调
+			startCallBack() {
+				if(this.isPrize) return
+				// 先开始旋转
+				if(this.prizeNum>0){
+					this.isPrize = true
+					this.prizeNum--
+					// this.$refs.myLucky.play()
+					this.prize()
+				}else{
+					return uni.showToast({
+						title:'抽奖次数不足',
+						icon:'none'
+					})
+				}
+				
+			},
+			// 抽奖结束触发回调
+			endCallBack(prize) {
+				this.isPrize = false
+				console.log(prize)
+				if(JSON.stringify(prize) == '{}') return
+				this.initLogs()
+				// 奖品详情
+				if(this.result.prizeType == 2){//视频会员
+					this.show = true
+				}else if(this.result.prizeType == 5){//未中奖
+					this.show3 = true
+				}else if(this.result.sendType == 2){//邮寄
+					this.show1 = true
+					this.$nextTick(() => {
+						this.$refs.uForm.setRules(this.rules)
+					})
+				}else if(this.result.sendType == 1 || this.result.sendType == 3){//线上线下发货
+					this.show2 = true
+				}
+			},
+			// 将两个数组交替插入一个数组
+			mergeArraysAlternately(arr1, arr2) {
+			  const result = [];
+			  const length1 = arr1.length;
+			  const length2 = arr2.length;
+			  let i = 0, j = 0;
+			
+			  while (i < length1 || j < length2) {
+			    if (i < length1) {
+			      result.push(arr1[i]);
+			      i++;
+			    }
+			    if (j < length2) {
+			      result.push(arr2[j]);
+			      j++;
+			    }
+			  }
+			
+			  return result;
+			},
+
+			getLuckyDrawDetail(raffleId) {
+				getLuckyDrawDetail({
+					raffleId,
+					regionCode:'520100'
+				}).then(res => {
+					if (res.state == 'Success') {
+						this.bg = res.content.backImg
+						this.rule =  res.content.raffleRule
+						let prizeList = [] // 奖品列表
+						let failList = []//未中奖列表
+						res.content.prizes.map(item=>{
+							if(item.prizeType == 5){
+								failList.push(item)
+							}else{
+								prizeList.push(item)
+							}
+						})
+						
+						let newPrizeList = this.mergeArraysAlternately(prizeList,failList)
+						
+						this.list = prizeList
+						
+						this.prizes = []
+						newPrizeList.map((item,index)=>{
+							this.prizes.push({
+								fonts: [{
+									text: item.prizeName,
+									top: '10%',
+									fontSize:'20rpx',
+									wordWrap:true,
+									lengthLimit:'70%',
+									fontColor:'#FF8A26'
+								}],
+								background: this.colorList[(index+1)%3] ,
+								imgs:[{
+									src:item.prizeIcon,
+									top: '50rpx',
+									width:'70rpx',
+								}]
+							})
+						})
+						
+					}
+				})
+			},
+			
+			initLogs(){
+				this.myPrizeList()
+				this.status = 'more'
+				this.logList = []
+				this.query.currentPage = 1
+				this.prizeList()
+			},
+			
+			prizeList(){
+				this.status = 'loading'
+				prizeList(this.query).then(res=>{
+					if (res.state == 'Success') {
+						this.logList = this.logList.concat(res.content.records)
+						if(this.logList.length>= res.content.total){
+							this.status = 'noMore'
+						}else{
+							this.status = 'more'
+							this.query.currentPage++
+						}
+					}
+				})
+			},
+			myPrizeList(){
+				prizeList({raffleId:this.query.raffleId,currentPage:1,pageSize:10,phoneNum:JSON.parse(uni.getStorageSync('userInfo')).phoneNum}).then(res=>{
+					if (res.state == 'Success') {
+						this.myLogList = res.content.records
+					}
+				})
+			},
+			prize(){
+				prize({raffleId:this.prizeId}).then(res=>{
+					this.remainder(this.query.raffleId)
+					if (res.state == 'Success') {
+						this.$refs.myLucky.play()
+						let id = res.content.rafflePrizeId
+						this.list.map((item,index)=>{
+							if(item.id == id){
+								this.result = item
+								this.prizeIndex = index
+							}
+						})
+						this.result.rafflePrizeId =  res.content.rafflePrizeId
+						this.result.rafflePrizeLogId =  res.content.rafflePrizeLogId
+						console.log(this.result);
+						setTimeout(()=>{
+							this.$refs.myLucky.stop(this.prizeIndex)
+						},500)
+					}else{
+						console.log('baoc');
+						this.isPrize = false
+						this.$refs.myLucky.stop()
+					}
+				})
+			},
+			// 视频会员领取
+			submit(){
+				if(!this.account){
+					return uni.showToast({
+						title:'请输入账号',
+						icon:'none'
+					})
+				}else if(!uni.$u.test.mobile(this.account)){
+					return uni.showToast({
+						title:'手机号错误',
+						icon:'none'
+					})
+				}else{
+					uni.showLoading({
+						title:'提交中'
+					})
+					sendVipPrize({accountType:1,mobile:this.account,productId:this.result.prizeContent,userId:this.userId,rafflePrizeLogId:this.result.rafflePrizeLogId,rafflePrizeId:this.result.rafflePrizeId}).then(res=>{
+						if (res.state == 'Success') {
+							uni.hideLoading({noConflict:true})
+							this.show1 = false
+							this.account=JSON.parse(uni.getStorageSync('userInfo')).phoneNum
+							uni.showToast({
+								title:'提交成功',
+								icon:'none'
+							})
+						}
+					})
+				}
+			},
+			// 邮寄信息
+			submit1(){
+				this.$refs.uForm.validate().then(res => {
+					uni.showLoading({
+						title:'提交中'
+					})
+					this.noteQuery.rafflePrizeLogId = this.result.rafflePrizeLogId
+					notes(this.noteQuery).then(res=>{
+						if (res.state == 'Success') {
+							this.show1 = false
+							uni.hideLoading({noConflict:true})
+							uni.showToast({
+								title:'领取成功',
+								icon:'none'
+							})
+						}
+					})
+				})
+				
+			},
+			remainder(raffleId){
+				if(this.isGet) return
+				this.isGet = true
+				remainder({raffleId}).then(res=>{
+					this.isGet = false
+					if (res.state == 'Success') {
+						this.prizeNum = res.content
+					}
+				})
+			}
+		},
+		mounted() {
+			// this.startCallBack()
+		},
+		onLoad(options) {
+			this.query.raffleId = options.id
+			this.prizeId = options.id
+			this.remainder(options.id)
+			this.getLuckyDrawDetail(options.id)
+			this.myPrizeList()
+			try{
+				this.userId=JSON.parse(uni.getStorageSync('userInfo')).userId
+				this.account=JSON.parse(uni.getStorageSync('userInfo')).phoneNum
+			}catch(e){
+				//TODO handle the exception
+				return uni.showModal({
+					title:'请登录',
+					confirmText:'去登录',
+					success(res){
+						console.log(res);
+						if(res.confirm){
+							uni.navigateTo({
+								url:'../../login/login/login?redirect=/luckyDraw/prizeList'
+							})
+						}
+					}
+				})
+			}
+		}
+
+	}
+</script>
+
+<style lang="scss">
+	.luckyDraw {
+		padding-top: 500rpx;
+		padding-bottom: 80rpx;
+		position: relative;
+		background-image: url('https://zswl-dev.oss-cn-chengdu.aliyuncs.com/63b7c68b71a69169d1b33f92/store/bdb/user/avatar/rP86PRmpk21z60fd17f0e1363aacf412d0a7f9983abb.png/1.png') ;
+		background-size: 750rpx ;
+		.rule-btn{
+			width: 56rpx;
+			height: 204rpx;
+			background: linear-gradient( 89deg, #FEBF37 0%, #FF8A26 100%);
+			box-shadow: 0rpx 4rpx 2rpx 2rpx rgba(249,115,29,0.63), inset 0rpx 6rpx 12rpx 2rpx rgba(255,255,255,0.49);
+			border-radius: 20rpx 0rpx 0rpx 20rpx;
+			border: 0rpx solid #FF8B26;
+			font-size: 28rpx;
+			color: #FFFFFF;
+			writing-mode: vertical-lr;
+			text-align: center;
+			display: flex;
+			align-items: center;
+			justify-content: center;
+			letter-spacing: 10rpx;
+			position: absolute;
+			right: 0%;
+			top: 430rpx;
+		}
+		.lucky-box {
+			z-index: 3;
+			.desk {
+				position: relative;
+				// top: -100rpx;
+				margin-top: -100rpx;
+				margin-bottom: 20rpx;
+				display: flex;
+				justify-content: center;
+
+				.icon {
+					width: 618rpx;
+					height: 234rpx;
+				}
+
+				.times {
+					position: absolute;
+					bottom: 22rpx;
+					left: 50%;
+					transform: translateX(-50%);
+					font-size: 36rpx;
+					color: #FFFFFF;
+				}
+			}
+		}
+		
+		.box{
+			width: 710rpx;
+			background: #FFE3A9;
+			box-shadow: 0rpx 2rpx 12rpx 2rpx #FF8B26, inset 0rpx 6rpx 12rpx 2rpx rgba(255,255,255,0.58);
+			border-radius: 16rpx;
+			margin: 0 auto 28rpx;
+			padding-bottom: 20rpx;
+			position: relative;
+			.title{
+				font-size: 48rpx;
+				color: #EE4320;
+				text-align: center;
+				padding: 20rpx 0;
+				.icon{
+					width: 192rpx;
+					height: 48rpx;
+				}
+			}
+			.more-btn{
+				font-size: 28rpx;
+				color: #FF8F2E;
+				position: absolute;
+				right: 24rpx;
+				top: 34rpx;
+			}
+			.content{
+				width: 668rpx;
+				max-height: 480rpx;
+				overflow: auto;
+				background: #FFFFFF;
+				border-radius: 16rpx 16rpx 16rpx 16rpx;
+				margin: 0 auto;
+				padding: 14rpx 28rpx;
+				box-sizing: border-box;
+				
+			}
+			.box-container{
+				.item{
+					font-size: 28rpx;
+					color: #222222;
+					padding: 12rpx 0;
+					line-height: 40rpx;
+					.yellow{
+						color: #FF8B26;
+					}
+				}
+			}
+			.flex-box{
+				display: flex;
+				flex-wrap: wrap;
+				padding: 14rpx 0;
+				.item{
+					width: 167rpx;
+					padding: 14rpx 0;
+					display: flex;
+					flex-direction: column;
+					align-items: center;
+					.icon{
+						width: 80rpx;
+						height: 80rpx;
+					}
+					.name{
+						width: 140rpx;
+						font-size: 24rpx;
+						color: #222222;
+						margin-top: 12rpx;
+						text-align: center;
+						overflow: hidden;
+						text-overflow: ellipsis;
+						/* 弹性伸缩盒子模型显示 */
+						display: -webkit-box;
+						/* 限制在一个块元素显示的文本的行数 */
+						-webkit-line-clamp: 2;
+						/* 设置或检索伸缩盒对象的子元素的排列方式 */
+						-webkit-box-orient: vertical;
+					}
+				}
+			}
+			
+			.table-box{
+				padding: 0;
+				.header{
+					padding: 14rpx 0;
+					font-size: 28rpx;
+					color: #222222;
+					display: flex;
+					align-items: center;
+					.num{
+						width: 120rpx;
+						text-align: center;
+					}
+					.time{
+						flex: 1;
+						text-align: center;
+					}
+					.info{
+						flex: 1;
+						text-align: center;
+					}
+					
+				}
+				.table-body{
+					max-height: 360rpx;
+					overflow: auto;
+					.item{
+						padding: 14rpx 0;
+						display: flex;
+						align-items: center;
+						font-size: 24rpx;
+						.num{
+							width: 120rpx;
+							text-align: center;
+						}
+						.time{
+							flex: 1;
+							text-align: center;
+						}
+						.info{
+							flex: 1;
+							color: #FF8B26;
+							text-align: center;
+						}
+					}
+				}
+			}
+		}
+
+		.vue-ref {
+			box-shadow: 0rpx 20rpx 20rpx 2rpx rgba(165, 81, 9, 0.3);
+		}
+
+		.hidden {
+			position: fixed;
+			top: 999999rpx;
+			left: 999999rpx;
+		}
+
+		.modal {
+			position: fixed;
+			top: 0%;
+			left: 0;
+			width: 100%;
+			height: 100%;
+			display: flex;
+			justify-content: center;
+			align-items: center;
+			background: rgba(0, 0, 0, .4);
+
+		}
+
+		.dialog-border {
+			width: 590rpx;
+			height: 672rpx;
+			background: #FF5642;
+			box-shadow: inset 0rpx 6rpx 12rpx 2rpx #FFFFFF;
+			border-radius: 24rpx 24rpx 24rpx 24rpx;
+			border: 1rpx solid #FFFFFF;
+			display: flex;
+			justify-content: center;
+			align-items: center;
+			position: relative;
+			.close{
+				width: 40rpx;
+				height: 40rpx;
+				position: absolute;
+				bottom: -70rpx;
+				left: calc(50% - 20rpx);
+			}
+
+			.top-icon {
+				width: 332rpx;
+				height: 142rpx;
+				position: absolute;
+				top: -130rpx;
+				left: calc(50% - 166rpx);
+				vertical-align: bottom;
+			}
+
+			.btn-box {
+				position: absolute;
+				left: 0%;
+				bottom: 0%;
+				width: 590rpx;
+				height: 134rpx;
+				background: linear-gradient(180deg, #FFF3E2 0%, #F4AF9E 100%);
+				border-radius: 0rpx 0rpx 24rpx 24rpx;
+				display: flex;
+				align-items: center;
+				justify-content: center;
+
+				.btn {
+					width: 258rpx;
+					height: 80rpx;
+					line-height: 80rpx;
+					text-align: center;
+					background: #FF4D3A;
+					box-shadow: 0rpx 6rpx 12rpx 2rpx #FF4D3A, inset 0rpx 16rpx 4rpx 2rpx #E8422F;
+					border-radius: 72rpx 72rpx 72rpx 72rpx;
+					border: 2rpx solid #FFFFFF;
+					font-weight: bold;
+					font-size: 32rpx;
+					color: #FFFFFF;
+
+				}
+				
+			}
+
+			.dialog-content {
+				width: 542rpx;
+				height: 612rpx;
+				overflow: auto;
+				background: linear-gradient(151deg, #FFFFFF 0%, #FFFFFF 0%, #FFD9BF 100%);
+				border-radius: 24rpx 24rpx 24rpx 24rpx;
+
+				.title-icon {
+					width: 430rpx;
+					height: 84rpx;
+					margin-left: 84rpx;
+					margin-top: 30rpx;
+				}
+			}
+		}
+		
+		// .result-box{
+		// 	width: 80%;
+		// 	text-align: center;
+		// 	margin: 0 auto;
+		// 	.goods-name{
+		// 		font-weight: bold;
+		// 		font-size: 28rpx;
+		// 		color: #222222;
+		// 	}
+		// 	.img{
+		// 		width: 168rpx;
+		// 		height: 168rpx;
+		// 		margin-top: 20rpx;
+		// 	}
+		// 	.address{
+		// 		font-weight: bold;
+		// 		font-size: 28rpx;
+		// 		color: #222222;
+		// 		margin-top: 20rpx;
+		// 	}
+		// 	.notice{
+		// 		font-size: 20rpx;
+		// 		color: #AAAAAA;
+		// 		margin-top: 20rpx;
+		// 	}
+		// }
+		
+		.result-box{
+			// width: 80%;
+			width: 90%;
+			text-align: center;
+			margin: 0 auto;
+			padding-bottom: 200rpx;
+			.goods-name{
+				font-weight: bold;
+				font-size: 28rpx;
+				color: #222222;
+			}
+			.img{
+				width: 168rpx;
+				height: 168rpx;
+				margin-top: 20rpx;
+			}
+			.sub-title{
+				font-size: 24rpx;
+				color: #222222;
+				margin-top: 20rpx;
+			}
+			.input-box{
+				display: flex;
+				font-size: 28rpx;
+				color: #222222;
+				margin-top: 20rpx;
+				.label{
+					font-weight: bold;
+					font-size: 28rpx;
+					color: #222222;
+					width: 350rpx;
+					white-space: nowrap;
+				}
+				.input{
+					font-size: 28rpx;
+					color: #222222;
+				}
+			}
+			.address{
+				font-weight: bold;
+				font-size: 28rpx;
+				color: #222222;
+				margin-top: 20rpx;
+			}
+			.notice{
+				font-size: 20rpx;
+				color: #AAAAAA;
+				margin-top: 20rpx;
+			}
+		}
+		
+		.rule-content{
+			padding: 0 28rpx 160rpx;
+			overflow: auto;
+			max-height: 380rpx;
+			.rule-text{
+				font-size: 24rpx;
+				color: #222222;
+				word-break: break-all;
+			}
+		}
+		
+	}
+</style>

+ 412 - 0
luckyDraw/prizeList.vue

@@ -0,0 +1,412 @@
+<template>
+	<view class="prizeList">
+		
+		<zs-list class="store-box" mt="0" @load="prizeList" :status="status">
+			<view class="item" v-for="(item,index) in list" :key="index">
+				<zs-img :src="item.prizeIcon" width="160rpx" height="160rpx" mode=""></zs-img>
+				
+				<view class="info">
+					<view class="title">
+						{{item.prizeName}}
+					</view>
+					
+					<view class="time">
+						中奖时间:{{item.createTime}}
+					</view>
+					<view class="valid">
+						有效期:中奖在{{item.valid}}天内领取,过期失效
+					</view>
+					
+					<view class="desc">
+						奖品说明:{{item.prizeMsg||'无'}}
+					</view>
+				</view>
+				<view class="btn-box">
+					<view class="btn" :class="[(item.expire == 2 || item.status == 2)?'disable':'']" @click="handleBtn(item)">
+						{{item  | filterStatus}}
+					</view>
+					<view class="type">
+						{{item.sendType |filterType}}
+					</view>
+				</view>
+				
+			</view>
+		</zs-list>
+		
+		<!-- 视频会员 -->
+		<zs-luck-dialog v-if="show" btnText="立即充值" @submit="submit" @close="handleClose('show')">
+			<view class="result-box">
+				<view class="goods-name">
+					{{prizeInfo.prizeName}}
+				</view>
+				<image class="img" :src="prizeInfo.prizeImg" mode=""></image>
+				
+				<view class="input-box">
+					<view class="label">
+						请输入充值手机号:
+					</view>
+					<input class="input" type="text" :maxlength="11" v-model.trim="account" placeholder="请输入充值账号" />
+				</view>
+				
+				<view class="notice">
+					请立即输入账号进行充值,若不充值则视为放弃
+				</view>
+			</view>
+		</zs-luck-dialog>
+		
+		<!-- 邮寄 -->
+		<zs-luck-dialog v-if="show1" btnText="提交收货信息" @submit="submit1" @close="handleClose('show1')">
+			<view class="result-box">
+				<view class="goods-name">
+					{{prizeInfo.prizeName}}
+				</view>
+				<image class="img" :src="prizeInfo.prizeImg" mode=""></image>
+				<view class="sub-title">
+					中奖奖品将邮寄给您,请输入收货信息
+				</view>
+				<u--form :model="noteQuery" ref="uForm" borderBottom labelWidth="180rpx" :labelStyle="{'color': '#222222','font-weight': 'bold','line-height':'60rpx','font-size': '28rpx'}">
+					<u-form-item label="收货人姓名" prop="notes.name" borderBottom ref="item1" required>
+						<u--input v-model.trim="noteQuery.notes.name" placeholder="请输入姓名" border="none"></u--input>
+					</u-form-item>
+					<u-form-item label="收货地址" prop="notes.address" borderBottom ref="item1" required>
+						<u--input v-model.trim="noteQuery.notes.address" :maxlength="50" placeholder="请输入收货地址" border="none"></u--input>
+					</u-form-item>
+					<u-form-item label="联系电话" prop="notes.phone" borderBottom ref="item1" required>
+						<u--input v-model.trim="noteQuery.notes.phone" :maxlength="11"  placeholder="请输入手机号" border="none"></u--input>
+					</u-form-item>
+				</u--form>
+				<view class="notice">
+					说明:为保障商品及时送到您的手中,请填写准确的地
+					址信息,若提交后需要修改地址信息请及时联系客服
+				</view>
+			</view>
+		</zs-luck-dialog>
+		
+		<!-- 线上发货 线下自提 -->
+		<zs-luck-dialog v-if="show2" @submit="handleClose('show2')" @close="handleClose('show2')">
+			<view class="result-box">
+				<view class="goods-name">
+					{{prizeInfo.prizeName}}
+				</view>
+				<image class="img" :src="prizeInfo.prizeImg" mode=""></image>
+				<view class="address">
+					{{prizeInfo.address}}
+				</view>
+				<view class="notice">
+					{{prizeInfo.sendType == 1?`兑换有效期:请在${prizeInfo.valid}个工作日内到自提点取货,过期失效`:'优惠劵已存入【我的优惠劵】,可前往【我的优惠劵】中查看使用'}}
+					
+				</view>
+			</view>
+		</zs-luck-dialog>
+
+	</view>
+</template>
+
+<script>
+	import {
+		prizeList,sendVipPrize,notes
+	} from "@/api/luckyDraw.js"
+	export default {
+		data() {
+			return {
+				status:'more',
+				prizeInfo:{},
+				account:'',
+				show:false,
+				show1:false,
+				show2:false,
+				list: [],
+				query:{
+					raffleId:0,
+					currentPage:1,
+					pageSize:10,
+					phoneNum:JSON.parse(uni.getStorageSync('userInfo')).phoneNum
+				},
+				userId:JSON.parse(uni.getStorageSync('userInfo')).userId,
+				noteQuery:{
+				  "notes": {
+				    "address": "",
+				    "name": "",
+				    "phone": ""
+				  },
+				  "rafflePrizeLogId": 0
+				},
+				rules:{
+					'notes.name': {
+						type: 'string',
+						required: true,
+						message: '请输入姓名',
+						trigger: ['blur', 'change']
+					},
+					'notes.address': {
+						type: 'string',
+						required: true,
+						message: '请输入收货地址',
+						trigger: ['blur', 'change']
+					},
+					'notes.phone': {
+						type: 'string',
+						required: true,
+						message: '请输入手机号',
+						trigger: ['blur', 'change']
+					},
+				}
+			}
+		},
+		filters: {
+			filterType: function(value) {
+				if(value == 1){
+					return '线下自提'
+				}else if(value == 2){
+					return '邮寄发货'
+				}else if(value == 3){
+					return '线上发货'
+				}
+			},
+			filterStatus(val){
+				if(val.expire == 2){
+					return '已过期'
+				}else if(val.status == 1){
+					return '未领取'
+				}else if(val.status == 2){
+					return '已领取'
+				}
+				
+			}
+		},
+		methods: {
+			handleBtn(item){
+				if(item.expire == 2 || item.status == 2){//已过期 或者已领取
+					return
+				}
+				
+				
+				if(item.prizeType == 2){//视频会员
+					this.show = true
+				}else if(item.sendType == 2){//邮寄
+					this.show1 = true
+					this.$nextTick(() => {
+						this.$refs.uForm.setRules(this.rules)
+					})
+				
+				}else{//线上发货 线下自提
+					this.show2 = true
+				}
+				this.prizeInfo = item
+				this.account = this.query.phoneNum
+			},
+			handleClose(key){
+				this[key] = false
+			},
+			// 视频会员领取
+			submit(){
+				if(!this.account){
+					return uni.showToast({
+						title:'请输入账号',
+						icon:'none'
+					})
+				}else if(!uni.$u.test.mobile(this.account)){
+					return uni.showToast({
+						title:'手机号错误',
+						icon:'none'
+					})
+				}else{
+					uni.showLoading({
+						title:'提交中'
+					})
+					sendVipPrize({accountType:1,mobile:this.account,productId:this.prizeInfo.prizeContent,userId:this.userId,rafflePrizeLogId:this.prizeInfo.rafflePrizeLogId,rafflePrizeId:this.prizeInfo.rafflePrizeId}).then(res=>{
+						if (res.state == 'Success') {
+							uni.hideLoading({noConflict:true})
+							this.show1 = false
+							this.init()
+							this.account = this.query.phoneNum
+							uni.showToast({
+								title:'领取成功',
+								icon:'none'
+							})
+						}
+					})
+				}
+			},
+			// 邮寄信息
+			submit1(){
+				this.$refs.uForm.validate().then(res => {
+					uni.showLoading({
+						title:'提交中'
+					})
+					this.noteQuery.rafflePrizeLogId = this.prizeInfo.rafflePrizeLogId
+					notes(this.noteQuery).then(res=>{
+						if (res.state == 'Success') {
+							this.show1 = false
+							uni.hideLoading({noConflict:true})
+							this.init()
+							uni.showToast({
+								title:'提交成功',
+								icon:'none'
+							})
+						}
+					})
+				})
+				
+			},
+			prizeList(){
+				this.status = 'loading'
+				prizeList(this.query).then(res=>{
+					if (res.state == 'Success') {
+						this.list = this.list.concat(res.content.records)
+						if(this.list.length>= res.content.total){
+							this.status = 'noMore'
+						}else{
+							this.status = 'more'
+							this.query.currentPage++
+						}
+					}
+				})
+			},
+			init(){
+				this.query.currentPage = 1
+				this.list = []
+				this.status = 'more'
+				this.prizeList()
+			}
+		},
+		onLoad(options) {
+			this.query.raffleId = options.id
+			try{
+				this.query.phoneNum=JSON.parse(uni.getStorageSync('userInfo')).phoneNum
+			}catch(e){
+				//TODO handle the exception
+				return uni.showModal({
+					title:'请登录',
+					confirmText:'去登录',
+					success(res){
+						console.log(res);
+						if(res.confirm){
+							uni.navigateTo({
+								url:'../../login/login/login?redirect=/luckyDraw/prizeList'
+							})
+						}
+					}
+				})
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+.prizeList{
+	padding: 20rpx 24rpx ;
+	background: #F9F9F9;
+	min-height: 100vh;
+	.item{
+		display: flex;
+		padding: 28rpx 24rpx;
+		background: #FFFFFF;
+		border-radius: 16rpx 16rpx 16rpx 16rpx;
+		margin-bottom: 20rpx;
+		.info{
+			padding-left: 20rpx;
+			flex: 1;
+			display: flex;
+			flex-direction: column;
+			justify-content: space-between;
+			.title{
+				font-weight: bold;
+				font-size: 28rpx;
+				color: #222222;
+			}
+			
+			.time{
+				font-size: 20rpx;
+				color: #AAAAAA;
+			}
+			
+			.valid{
+				font-size: 20rpx;
+				color: #AAAAAA;
+			}
+			.desc{
+				font-size: 20rpx;
+				color: #AAAAAA;
+			}
+		}
+		
+		.btn-box{
+			width: 120rpx;
+			text-align: center;
+			.btn{
+				width: 120rpx;
+				height: 48rpx;
+				line-height: 48rpx;
+				background: #EE4320;
+				border-radius: 28rpx 28rpx 28rpx 28rpx;
+				font-size: 24rpx;
+				color: #FFFFFF;
+				text-align: center;
+				
+			}
+			.btn.disable{
+				background: #F0F0F0;
+				color: #AAAAAA;
+			}
+			
+			.type{
+				font-size: 24rpx;
+				color: #3B83FF;
+				margin-top: 20rpx;
+			}
+			
+		}
+	}
+	
+	.result-box{
+		width: 90%;
+		text-align: center;
+		margin: 0 auto;
+		padding-bottom: 200rpx;
+		.goods-name{
+			font-weight: bold;
+			font-size: 28rpx;
+			color: #222222;
+		}
+		.img{
+			width: 168rpx;
+			height: 168rpx;
+			margin-top: 20rpx;
+		}
+		.sub-title{
+			font-size: 24rpx;
+			color: #222222;
+			margin-top: 20rpx;
+		}
+		.input-box{
+			display: flex;
+			font-size: 28rpx;
+			color: #222222;
+			margin-top: 20rpx;
+			.label{
+				font-weight: bold;
+				font-size: 28rpx;
+				color: #222222;
+				width: 350rpx;
+				white-space: nowrap;
+			}
+			.input{
+				font-size: 28rpx;
+				color: #222222;
+			}
+		}
+		.address{
+			font-weight: bold;
+			font-size: 28rpx;
+			color: #222222;
+			margin-top: 20rpx;
+		}
+		.notice{
+			font-size: 20rpx;
+			color: #AAAAAA;
+			margin-top: 20rpx;
+		}
+	}
+}
+</style>

+ 3 - 3
movie/index.vue

@@ -43,7 +43,7 @@
 							今天 269家影院放映2507场
 						</view> -->
 					</view>
-					<button class="buy-btn" type="default" @click="buy(item.movieId)">购票</button>
+					<button class="buy-btn" type="default">购票</button>
 				</view>
 			</template>
 			
@@ -100,7 +100,7 @@
 							{{ $u.timeFormat(new Date(item.releaseTime).getTime(), 'yyyy年mm月dd日')}}
 						</view>
 					</view>
-					<button class="presell-btn" type="default" @click="buy(item.movieId)">预售</button>
+					<button class="presell-btn" type="default">预售</button>
 				</view>
 			</template>
 		</zs-list>
@@ -142,7 +142,7 @@
 		},
 		methods: {
 			back(){
-				uni.reLaunch({
+				uni.switchTab({
 					url:'/pages/index/index'
 				})
 			},

+ 8 - 1
my/coupon/index.vue

@@ -14,7 +14,7 @@
 		<!-- 列表 -->
 		<zs-list class="store-box" mt="84rpx" @load="loadMore" :status="status">
 			
-			<view class="discounts-item" :class="[query.status != 1?'disabled':'']" v-for="(item,index) in list" :key="index" >
+			<view class="discounts-item" :class="[query.status != 1?'disabled':'']" v-for="(item,index) in list" :key="index" @click="goUse">
 					<view class="type-box">
 						<view class="price">
 							{{item | filterPrice}}
@@ -99,6 +99,13 @@
 			},
 		},
 		methods: {
+			goUse(){
+				if(this.query.status == 1){
+					uni.navigateTo({
+						url:'/refuel/index'
+					})
+				}
+			},
 			handleTab(val){
 				this.query.status = val
 				this.query.currentPage = 1

+ 16 - 3
my/memberCenter/index.vue

@@ -134,9 +134,9 @@
 								<view class="card-title">
 									{{item.couponName}}
 								</view>
-								<view class="limit">
+							<!-- 	<view class="limit">
 									{{item.useStartTime}}后可用
-								</view>
+								</view> -->
 								
 								<view class="desc">
 									{{item.usrDetail || '-'}}
@@ -249,7 +249,8 @@
 				show1:false,
 				item:{},
 				equityId:0,
-				equityContentId:0
+				equityContentId:0,
+				productId:0
 			}
 		},
 		computed: {
@@ -304,6 +305,7 @@
 					this.show = false
 					this.show1 = true
 					this.equityContentId = item.id
+					this.productId = item.serviceId
 					this.form.account = JSON.parse(uni.getStorageSync('userInfo')).phoneNum
 				}else{
 					uni.navigateTo({
@@ -318,6 +320,11 @@
 				
 			},
 			openPop(item){
+				console.log(item);
+				if(this.userInfo.setMealCode != item.setMealCode) return uni.showToast({
+					title:'不是当前会员',
+					icon:'none'
+				})
 				this.type = item.bindService
 				if(item.bindService == 2){//加油券
 					this.show = true
@@ -337,6 +344,10 @@
 							this.item.equityMsg = item.equityMsg.replaceAll('\n','<br/>')
 						}
 					})
+				}else if(item.bindService == 5){//抽奖
+					uni.navigateTo({
+						url:'/luckyDraw/index?id='+item.bindContent
+					})
 				}else if(item.equityName.indexOf('领水')!=-1){
 					uni.navigateTo({
 						url:'/special/water'
@@ -363,6 +374,8 @@
 			getMediaVipCoupon(){
 				let userInfo = JSON.parse(uni.getStorageSync('userInfo'))
 				getMediaVipCoupon({
+					productId:this.productId,
+					accountType:1,
 				   equityContentId:this.equityContentId,
 				  "equityId": this.equityId,
 				  "userId": userInfo.userId,

+ 1 - 1
my/memberCenter/logList.vue

@@ -8,7 +8,7 @@
 						{{JSON.parse(item.equityContent).equityName}}
 					</view>
 					<view class="time">
-						领取时间:{{JSON.parse(item.equityContent).createTime}}
+						领取时间:{{item.createTime}}
 					</view>
 				</view>
 				<view class="status" :class="[item.status == 1?'':'used']">

+ 7 - 33
my/order/detail.vue

@@ -1,4 +1,4 @@
-<template>
+<!-- <template>
 	<view class="order-detail">
 		<zs-skeleton type="orderDetail" :loading="pageLoading"></zs-skeleton>
 			<view class="status-box">
@@ -86,9 +86,6 @@
 					<view class="title">
 						券码信息
 					</view>
-				<!-- <view class="code-btn"  @click="checkCode">
-					查看券码 <image class="jiantou" src="../../static/jiantou-icon.png" mode=""></image>
-				</view> -->
 					
 					<uqrcode class="qrcode" ref="uqrcode" type="2d" auto canvas-id="qrcode" :value="codeData" :loading="loading" :options="{ margin: 20 }">
 						 <template v-slot:loading>
@@ -96,29 +93,6 @@
 						  </template>
 					</uqrcode>
 			</view>
-			<!-- <view class="content" v-if="info.goodsList[0].goodsState == 'USED'">
-				<view class="title">
-					消费记录
-				</view>
-				
-				<view class="item">
-					<view class="label">
-						消费时间
-					</view>
-					<view class="value">
-						{{$u.timeFormat(info.goodsList[0].verifyModel.checkTime,'yyyy-mm-dd hh:MM:ss')}}
-					</view>
-				</view>
-				<view class="item">
-					<view class="label">
-						消费门店
-					</view>
-					<view class="value">
-						{{info.goodsList[0].verifyModel.shopName}}
-					</view>
-				</view>
-				
-			</view> -->
 			
 			<view class="content">
 				<view class="title">
@@ -269,7 +243,7 @@
 					立即支付
 				</button>
 			</view>
-	<!-- 		<button type="default" :loading="btnLoading" class="pay-btn" v-if="info.goodsList[0].goodsState == 'WAIT_PAYMENT'&&info.goodsList[0].jobFlowMap !== 'XiaoJu'" @click="pay" >立即支付</button> -->
+
 			
 			
 			<view class="content apply-box" v-else-if="(info.goodsList[0].goodsState == 'WAIT_USE'||info.goodsList[0].goodsState == 'USED')&&info.goodsList[0].jobFlowMap == 'XiaoJu'" @click="handleCall">
@@ -303,7 +277,6 @@
 			
 			
 			
-			<!-- 二维码 -->
 			<u-overlay :show="show" v-if="show" :opacity="0.6">
 				<view class="wrap"  @click.stop="close">
 					<uqrcode ref="uqrcode" type="2d" auto canvas-id="qrcode" :value="codeData" :loading="loading" :options="{ margin: 20 }">
@@ -953,7 +926,7 @@
 	}
 	
 }
-</style>
+</style> -->
 
 
 
@@ -964,7 +937,7 @@
 
 
 
-<!-- <template>
+<template>
 	<view class="order-detail">
 		<zs-skeleton type="orderDetail" :loading="pageLoading"></zs-skeleton>
 			<view class="status-box">
@@ -1029,7 +1002,8 @@
 			
 			<view class="content info" v-if="info.goodsList[0].jobFlowMap !== 'XiaoJu'">
 				<view class="order-info">
-					<image class="icon" :src="info.goodsList[0].goodsInfo.goodsPath" mode=""></image>
+					<image class="icon" v-if="isVisual" src="http://hyxhsh.oss-cn-chengdu.aliyuncs.com/63b7c68b71a69169d1b33f92/store/bdb/user/avatar/6EvwLmvezXpS0e89f10d7fa6a30c6ce5bdcb7813b326.png/1.png" mode=""></image>
+					<image class="icon" v-else :src="info.goodsList[0].goodsInfo.goodsPath" mode=""></image>
 					<view class="shop-info">
 						<view class="title">
 							{{info.goodsList[0].goodsInfo.goodsName}}
@@ -1900,4 +1874,4 @@
 	}
 	
 }
-</style> -->
+</style>

+ 3 - 2
my/order/hotel/detail.vue

@@ -9,10 +9,10 @@
 					<view class="status" v-else>
 						{{info.goodsList[0].extend.orderInfo.orderStatus}}
 					</view>
-					<!-- <view class="notice" v-if="info.goodsList[0].goodsState == 'APPLY_REFUND'">
+					<view class="notice" v-if="info.goodsList[0].goodsState == 'REFUNDED'&&info.goodsList[0].extend.orderInfo.orderStatus=='已取消'">
 						订单已取消。正在为您操作退款,预计1~7个工作日原路退回到
 						您的帐户。
-					</view> -->
+					</view>
 				</view>
 			</view>
 			
@@ -596,6 +596,7 @@
 			}
 			.notice{
 				font-size: 24rpx;
+				color: #AAAAAA;
 				margin-top: 20rpx;
 			}
 		}

+ 3 - 1
my/order/index.vue

@@ -121,6 +121,8 @@
 					return this.imgUrl[item.goodsInfo.ot] 
 				}else if(item.jobFlowMap == 'Activity'){
 					return item.goodsInfo.activityCover
+				}else if(item.jobFlowMap == 'video2'){
+					return 'https://hyxhsh.oss-cn-chengdu.aliyuncs.com/63b7c68b71a69169d1b33f92/store/bdb/user/avatar/6EvwLmvezXpS0e89f10d7fa6a30c6ce5bdcb7813b326.png/1.png'
 				}else{
 					return item.goodsInfo.goodsPath
 				}
@@ -300,7 +302,7 @@
 						}
 						.price{
 							font-weight: bold;
-							color: #181818;
+							color: $uni-color-primary;
 						}
 
 					}

+ 4 - 8
my/order/refund.vue

@@ -16,19 +16,15 @@
 					<view class="title">
 						{{info.goodsList[0].goodsInfo.goodsName}}
 					</view>
-					<!-- 电影订单 -->
-		<!-- 			<view class="time">
-						2024-03-04 22:55
-					</view>
-					<view class="desc">
-						2号厅 4排4座 4排5座  4排6座  4排7座
-					</view> -->
 					<view class="goods-desc">
 					{{info.goodsList[0].goodsInfo.goodsDescribe}}
 					</view>
-					<view class="price">
+					<view class="price"v-if="!isVisual">
 						¥{{info.goodsList[0].goodsInfo.realPrice}}
 					</view>
+					<view class="price" v-else>
+						¥{{info.payAmount}}
+					</view>
 						
 
 				</view>

+ 33 - 22
my/order/refundDetail.vue

@@ -14,7 +14,7 @@
 					</u-steps>
 			</view>
 			
-			<view class="content shop-box">
+			<view class="content shop-box" v-if="!isVisual">
 				<view class="shop-name">
 					{{info.shopInfo.shopName}}
 				</view>
@@ -25,19 +25,26 @@
 			
 			<view class="content info">
 				<view class="order-info">
-					<image class="icon" :src="info.goodsList[0].goodsInfo.goodsPath" mode=""></image>
+					<image class="icon" v-if="isVisual" src="http://hyxhsh.oss-cn-chengdu.aliyuncs.com/63b7c68b71a69169d1b33f92/store/bdb/user/avatar/6EvwLmvezXpS0e89f10d7fa6a30c6ce5bdcb7813b326.png/1.png" mode=""></image>
+					<image class="icon" v-else :src="info.goodsList[0].goodsInfo.goodsPath" mode=""></image>
 					<view class="shop-info">
 						<view class="title">
 							{{info.goodsList[0].goodsInfo.goodsName}}
 						</view>
-						<view class="goods-desc">
-						{{info.goodsList[0].goodsInfo.goodsDescribe}}
-						</view>
-						<view class="price-box">
-							<view class="price">
+						<template v-if="!isVisual">
+							<view class="goods-desc">
+								{{info.goodsList[0].goodsInfo.goodsDescribe}}
+							</view>
+							<view class="price-box">
+								<view class="price">
 									¥{{info.goodsList[0].goodsInfo.realPrice}}
+								</view>
+							</view>
+						</template>
+						<view class="price-box" v-else>
+							<view class="price">
+								¥{{info.payAmount}}
 							</view>
-							
 						</view>
 						
 						
@@ -61,7 +68,7 @@
 						商品金额
 					</view>
 					<view class="value black">
-						¥{{info.goodsList[0].goodsInfo.realPrice}}
+						¥{{info.goodsList[0].goodsInfo.realPrice||info.payAmount}}
 					</view>
 				</view>
 				
@@ -199,22 +206,26 @@
 			payDetails(orderNo){
 				payDetails(orderNo).then(res=>{
 					this.info = res.content
-					console.log(this.info);
 					if(this.info.goodsList[0].goodsState == 'REFUNDED'&&this.info.goodsList[0].refundLog.conclusionTime){
-						console.log(222222222222222);
-						this.stepList.unshift({
-							desc:'退款完成,已原路返回',
-							time:'conclusionTime'
-						})
-					}
-					try{
-						let extend = JSON.parse(this.info.extend)
-						if(extend.account){
-							this.isVisual = true
+						if(this.stepList.length == 2){
+							this.stepList.unshift({
+								desc:'审核通过,到账时间以实际到账为准',
+								time:'conclusionTime'
+							})
 						}
-					}catch(e){
-						//TODO handle the exception
 					}
+					
+					if(this.info.goodsList[0].jobFlowMap =='video2'||this.info.goodsList[0].jobFlowMap =='P802'){
+						this.isVisual = true
+					}
+					// try{
+					// 	let extend = JSON.parse(this.info.extend)
+					// 	if(extend.account){
+					// 		this.isVisual = true
+					// 	}
+					// }catch(e){
+					// 	//TODO handle the exception
+					// }
 				})
 			},
 			// 支付

+ 1 - 1
my/order/scenic/refundDetail.vue

@@ -169,7 +169,7 @@
 					if(this.info.goodsList[0].goodsState == 'REFUNDED'&&this.info.goodsList[0].refundLog.conclusionTime){
 						console.log(222222222222222);
 						this.stepList.unshift({
-							desc:'退款完成,已原路返回',
+							desc:'审核通过,到账时间以实际到账为准',
 							time:'conclusionTime'
 						})
 					}

+ 1 - 1
my/order/signUp/refundDetail.vue

@@ -190,7 +190,7 @@
 					console.log(this.info);
 					if(this.info.goodsList[0].goodsState == 'REFUNDED'&&this.info.goodsList[0].refundLog.conclusionTime){
 						this.stepList.unshift({
-							desc:'退款完成,已原路返回',
+							desc:'审核通过,到账时间以实际到账为准',
 							time:'conclusionTime'
 						})
 					}

+ 17 - 5
orderFood/index.vue

@@ -172,6 +172,7 @@
 				scrollTo1:'',
 				topList:[],
 				offsetTop:0,//顶部距离
+				cateName:'',//跳转过来携带的分类名称
 			};
 		},
 		computed: {
@@ -215,10 +216,10 @@
 			},
 			add(item,num){
 				if(this.carList.some(i=>item.productId == i.productId)){
-					this.carList.forEach((i,index)=>{
-						if(item.productId == i.productId){
-							item.quantity+=num
-							if(item.quantity == 0){
+					this.carList.forEach((row,index)=>{
+						if(item.productId == row.productId){
+							row.quantity+=num
+							if(row.quantity == 0){
 								this.carList.splice(index,1)
 							}
 						}
@@ -249,6 +250,14 @@
 				menuList(this.shopId).then(res=>{
 					if(res.state = 'Success'){
 						this.list = res.content.data
+						if(this.cateName){
+							this.list.map((item,index)=>{
+								if(item.topName == this.cateName){
+									this.handleTab(index)
+								}
+							})
+						}
+						
 						this.$nextTick(()=>{
 							let query = uni.createSelectorQuery().in(this);
 							query.selectAll('.section-name').boundingClientRect(data=>{
@@ -257,6 +266,8 @@
 									return item.top
 								})
 							}).exec();
+							
+							
 						})
 					}
 				})
@@ -322,7 +333,8 @@
 			}
 		},
 		onLoad(options) {
-			this.shopId = options.shopId || "63836999-5117-4980-8632-497f007808ef"
+			this.cateName = options.cateName
+			this.shopId = options.id || "63836999-5117-4980-8632-497f007808ef"
 			this.shopDetail()
 			this.menuList()
 		},

+ 17 - 0
pages.json

@@ -729,6 +729,23 @@
 					}
 				}
 			]
+		},
+		{
+			"root": "luckyDraw",
+			"pages": [
+				{
+					"path": "index",
+					"style": {
+						"navigationBarTitleText": "抽奖"
+					}
+				},
+				{
+					"path": "prizeList",
+					"style": {
+						"navigationBarTitleText": "我的奖品"
+					}
+				}
+			]
 		}
 
 	],

+ 23 - 40
pages/index/index.vue

@@ -287,13 +287,14 @@
 								type: 'gcj02',
 								success: (res) => {
 									// 解析地址
-									that.query.latitude = res.latitude
-									that.query.longitude = res.longitude
+									that.query.latitude = res.latitude.toFixed(5)
+									that.query.longitude = res.longitude.toFixed(5)
 									// that.query['location.lat'] = res.latitude
 									// that.query['location.lon'] = res.longitude
 									// 存储经纬度
-									uni.setStorageSync('location',JSON.stringify({latitude:res.latitude,longitude:res.longitude}))
+									uni.setStorageSync('location',JSON.stringify({latitude:res.latitude.toFixed(5),longitude:res.longitude.toFixed(5)}))
 									if(that.query.regionCode){//选择了城市
+									uni.setStorageSync('regionCode',that.query.regionCode)
 									console.log('选择了城市',that.city);
 										that.city = uni.getStorageSync('HomeCity') || uni.getStorageSync('city')
 										getDistrict({boundary:1,keyword:that.city,sub_admin:1,extensions_code:1}).then(r=>{
@@ -310,14 +311,16 @@
 										})
 									}else{//没选择城市
 									console.log('没选择城市',that.city);
+									uni.setStorageSync('HomeCity','')
 									queryFromLocation({
 												coordType:'gcj02ll',
-												lat: res.latitude,
-												lng: res.longitude
+												lat: that.query.latitude,
+												lng: that.query.longitude
 											}).then(res=>{
 										console.log('解析结果',res);
 										if(res.state == 'Success'){
 											that.query.regionCode = res.content.geoAddressComponent.adcode.substr(0,4) +'00'
+											uni.setStorageSync('regionCode',that.query.regionCode)
 											that.city = res.content.geoAddressComponent.city
 											uni.setStorageSync('city',res.content.geoAddressComponent.city)
 											getDistrict({boundary:1,keyword:that.city,sub_admin:1,extensions_code:1}).then(r=>{
@@ -392,6 +395,9 @@
 			goGoodsDetail(item) {
 				// uni.setStorageSync('shopInfo', JSON.stringify(item))
 				let url = ''
+				let cateName = '' //大牌点餐类型名
+				let id = item.pid
+				let epId = item.id
 				if (item.productType === 'Web') {
 					url = item.meta.url
 				    console.log('链接');
@@ -422,8 +428,10 @@
 					url = '/detail/virtualGoods/index'
 				    console.log('餐厅');
 				} else if (item.productType === 'RestaurantFood') {
-					url = '/detail/discountsDetail/index'
-				    console.log('餐厅美食');
+					url = '/orderFood/index'
+					id = item.meta.menus.shop.id
+					cateName = item.meta.menus.cate_name
+				    console.log('大牌点餐');
 				} else if (item.productType === 'CarMaintain') {
 					url = '/detail/discountsDetail/index'
 				    console.log('汽车保养门店');
@@ -455,9 +463,11 @@
 				    // 处理未知或未定义的产品类型
 				    console.log('未知产品类型');
 				}
+				
+				
 				uni.navigateTo({
 					// url: `../../detail/goodsDetail/index?id=${item.goodsVos[0].goodsId}`
-					url: `${url}?id=${item.pid}&epId=${item.id}`
+					url: `${url}?id=${id}&epId=${epId}&cateName=${cateName}`
 				})
 	
 			},
@@ -485,8 +495,8 @@
 				if(this.status == 'noMore' || this.status == 'loading') return
 				this.status = 'loading'
 				homeSearch(this.query).then(res=>{
+					this.loading = false
 					if(res.state == 'Success'){
-						this.loading = false
 						let list = []
 						let list1 = []
 						let data = res.content.content
@@ -504,9 +514,6 @@
 						// 存储原始数据
 						this.list = this.list.concat(list)
 						this.list1 = this.list1.concat(list1)
-						
-						// this.list = JSON.parse(JSON.stringify(this.copyList))
-						// this.list1 = JSON.parse(JSON.stringify(this.copyList1))
 						let total = this.list.length + this.list1.length
 						if(total >= res.content.totalElements){
 							this.status = 'noMore'
@@ -514,32 +521,6 @@
 							this.status = 'more'
 							this.query.page++
 						}
-						// if(!this.advList1.length){
-						// 	this.getGoodsAdv().then(()=>{
-						// 		this.advList1.map(item=>{
-						// 			if(item.showSort<= (this.list.length + this.list1.length)){
-						// 				if((item.showSort-1)%2 == 0){
-						// 					this.list.splice(Math.ceil(item.showSort/2)-1,0,{isAdv:true,...item})
-						// 				}else{
-						// 					this.list1.splice(Math.ceil(item.showSort/2)-1,0,{isAdv:true,...item})
-						// 				}
-						// 			}
-						// 		})
-						// 	}
-						//   )
-						  
-						// }else{
-						// 	this.advList1.map(item=>{
-						// 		if(item.showSort<= (this.list.length + this.list1.length)){
-						// 			if((item.showSort-1)%2 == 0){
-						// 				this.list.splice(Math.ceil(item.showSort/2)-1,0,{isAdv:true,...item})
-						// 			}else{
-						// 				this.list1.splice(Math.ceil(item.showSort/2)-1,0,{isAdv:true,...item})
-						// 			}
-						// 		}
-						// 	})
-						// }
-						
 					}
 				})
 			},
@@ -663,9 +644,11 @@
 						title: '已经是平台用户',
 						icon: 'none'
 					});
-					return
+					// return
+				}else{
+					uni.setStorageSync('inviteCode',decodeURIComponent(query.scene))
 				}
-				uni.setStorageSync('inviteCode',decodeURIComponent(query.scene))
+				// uni.setStorageSync('inviteCode',decodeURIComponent(query.scene))
 			}
 			
 			this.query.regionCode = query.regionCode || ''

+ 8 - 5
pay/pay.vue

@@ -253,12 +253,15 @@
 													title: '取消支付',
 													icon: 'fail'
 												})
-												// 取消支付后,获取支付信息以备再次支付
-												payDetails(that.query.orderNo).then(r => {
-													if (r.state == 'Success') {
-													that.payData = JSON.parse(r.content.miniPayRequest)
-													}
+												uni.reLaunch({
+													url:'/my/order/detail?id='+that.query.orderNo
 												})
+												// 取消支付后,获取支付信息以备再次支付
+												// payDetails(that.query.orderNo).then(r => {
+												// 	if (r.state == 'Success') {
+												// 	that.payData = JSON.parse(r.content.miniPayRequest)
+												// 	}
+												// })
 											}
 										})
 

+ 1 - 1
refuel/coupon.vue

@@ -17,7 +17,7 @@
 							{{item.promotionDesc}}
 						</view>
 						<view class="limit">
-							{{$u.timeFormat(item.startTimestamp*1000, 'yyyy-mm-dd hh:MM:ss') }}后可用
+							{{$u.timeFormat(item.startTimestamp*1000, 'yyyy-mm-dd hh:MM:ss') }}后
 						</view>
 						<view class="desc">
 							{{item.statusDesc || '-'}}

+ 7 - 8
refuel/refuelDetail.vue

@@ -9,23 +9,23 @@
 			<view class="block shopInfo">
 				<view class="title-box">
 					<view class="title">
-						{{info.storeName}}
+						{{info.storeName || ''}}
 					</view>
 					<view class="diatance-box">
 						<image class="icon" src="../static/refuel/nav-icon-black.png" mode=""></image>
-						{{(distance/1000).toFixed(2)}}km
+						{{distance?(distance/1000).toFixed(2):0}}km
 					</view>
 				</view>
 				<view class="address">
-					{{info.address}}
+					{{info.address || ''}}
 				</view>
 				<view class="time">
-					营业时间 周一至周日  {{info.openTimeHourStart}}:00 - {{info.openTimeHourEnd}}:00
+					营业时间 周一至周日  {{info.openTimeHourStart || ''}}:00 - {{info.openTimeHourEnd || ''}}:00
 				</view>
 				
 				<view class="address-box">
 					<view class="distance">
-						距离您{{(distance/1000).toFixed(2)}}公里
+						距离您{{distance?(distance/1000).toFixed(2):0}}公里
 					</view>
 					
 					<view class="btn-box">
@@ -591,10 +591,9 @@
 		},
 		onLoad(options) {
 			this.epId = options.epId
-			console.log(111,options);
 			// '5154031457384536897'
-			this.storeId =  options.id
-			this.queryStoreDetail(options.id)
+			this.storeId = '5154031457384536897'|| options.id
+			this.queryStoreDetail('5154031457384536897'||options.id)
 			const eventChannel = this.getOpenerEventChannel();
 			let that = this
 			eventChannel.on('pay', function(data) {

+ 249 - 0
uni_modules/almost-lottery/changelog.md

@@ -0,0 +1,249 @@
+## 1.9.7(2023-08-02)
+本次更新:
+- 调整 `draw-before` 为自定义函数,且该函数为必备函数,转盘能否启动,将根据该函数中调用 `callback` 时传递的 `Boolean` 进行判断
+## 1.9.6.2(2023-07-28)
+本次更新:
+- 更新示例项目
+## 1.9.6.1(2023-07-28)
+本次更新:
+- 修复参数文档排版问题
+## 1.9.6(2023-07-28)
+本次更新:
+- 新增`drawStartBefore`钩子,请看说明文档
+## 1.9.5(2023-07-12)
+本次更新:
+- 优化 `duration` 参数的变更需要刷新才能生效的开发体验
+- 新增 `duration` 和 `ringCount` 设置不合理时的 `console` 提醒
+## 1.9.4(2023-07-06)
+本次更新:
+- 修复某些情况文字无法换行的问题
+- 参数`strMaxLen`设置为`0`时不限制文字长度
+## 1.9.3(2023-06-12)
+本次更新:
+- 新增 `selfTime` 参数,查看文档说明
+- 示例项目分离 `uni-popup` 用例为独立页面进行展示
+## 1.9.2(2023-05-22)
+本次更新:
+- 新增 `renderDelay` 参数,请查看文档
+- 示例项目新增 `ui-popup` 组件的用例,使用 `uni-popup` 包裹转盘时,请前往 `uni-popup` 组件文档关注平台兼容性问题
+## 1.9.1(2023-03-07)
+本次更新:
+- 新增 `selfRotaty` 自转参数,视觉效果上存在细微瑕疵,欢迎PR
+## 1.8.33(2022-07-04)
+本次更新:
+- 统一奖品图片下载方式
+## 1.8.32(2022-06-20)
+本次更新:
+- 调整平台兼容性(因 HBX 存在 bug,导致平台兼容性的更新多次失败,近几次的更新都可以忽略)
+## 1.8.31(2022-06-20)
+本次更新:
+- 无意义的更新,请忽略
+## 1.8.30(2022-06-20)
+本次更新:
+- 调整平台兼容性
+## 1.8.29(2022-06-19)
+本次更新:
+- 调整平台兼容信息
+## 1.8.28(2022-06-19)
+本次更新:
+- 修复单个尺寸过大的 canvas 在 H5/APP-vue iOS/Safari 中存在可能无法绘制成功的问题
+## 1.8.27(2022-06-01)
+本次更新:
+- 优化示例项目
+## 1.8.26(2022-06-01)
+本次更新:
+- 修复奖品图片裁切为圆形时在安卓机器不显示的问题
+## 1.8.25(2022-05-31)
+本次更新:
+- 修复部分安卓下载图片得到.unknown格式文件的问题
+## 1.8.24(2022-05-09)
+本地缓存:
+- 优化示例项目
+## 1.8.23(2022-05-09)
+本地缓存:
+- 优化清除文件缓存的方法
+## 1.8.22(2022-05-09)
+本次更新:
+- 调整计算转盘绘制的方式
+## 1.8.21(2022-05-08)
+本次更新:
+- 调整示例项目中本地图片的引入方式
+## 1.8.20(2022-04-29)
+本次更新:
+- 修复转盘在某个临界点可以出现多次触发的问题
+## 1.8.19(2022-04-27)
+本次更新:
+- 奖品文字的绘制由先前的两行变成多行,根据设定的每行文字的长度分段绘制
+## 1.8.18(2022-04-25)
+本次更新:
+- 减少小程序平台的 delay
+## 1.8.17(2022-03-23)
+本次更新:
+- 新增配置项 `imgCircled` 奖品图片是否裁切为圆形,默认不裁切
+## 1.8.16(2022-03-04)
+本次更新:
+- 示例项目新增绘制时长的计算,方便开发时定位绘制慢的问题
+## 1.8.15(2022-03-02)
+本次更新:
+- 优化一处错误提示信息的展现方式
+## 1.8.14(2021-11-29)
+本次更新:
+- 示例项目中新增开放自定义权重最大值,没有自定义则取权重数组中的最大值
+- 更新文档
+## 1.8.13(2021-11-03)
+本次更新:
+- 注释 `1.8.12` 版本中调试时的代码
+## 1.8.12(2021-11-03)
+本次更新:
+- 修复一些老机型不支持 `flex` 导致布局错乱的问题
+## 1.8.11(2021-10-29)
+本次更新:
+- 优化示例项目中模拟接口访问的速度
+## 1.8.10(2021-10-19)
+本次更新:
+- 优化组件代码
+- 更新示例项目
+## 1.8.9(2021-09-28)
+本次更新:
+- 移除内置的 `奖品准备中...` 提示
+## 1.8.8(2021-09-27)
+本次更新:
+- 修复 `1.8.6` 引起的非微信小程序平台绘制异常的问题
+## 1.8.7(2021-09-23)
+本次更新:
+- 优化项目中使用到的图片大小
+## 1.8.6(2021-09-23)
+本次更新:
+- 修复小程序平台在绘制 `base64` 格式的图片时无法在真机模式下正常显示的问题
+## 1.8.5(2021-09-13)
+本次更新:
+- 修复一个已知问题
+## 1.8.4(2021-09-12)
+本次更新:
+- 调整 `strFontColor` 为 `strFontColors`,现在可以设置每个区块的文字颜色,详见文档说明
+## 1.8.3(2021-09-12)
+本次更新:
+- 修复因 `1.8.0` 改动引起的文字方向、无奖品图时绘制异常的问题
+- 新增 `imgDrawed` 是否绘制奖品图片的配置项 ,默认为 `true`
+## 1.8.2(2021-09-10)
+本次更新:
+**不兼容旧版本的更新**
+- 移除配置项 `strKey` 字段
+- 调整 `prizeList` 结构
+## 1.8.1(2021-09-06)
+本次更新:
+- 修复 `hbx3.1.22` 在小程序平台处理 `id-name` 存在解析错误的问题
+## 1.8.0(2021-09-06)
+本次更新:
+**该版本更新涉及破坏性的变更,请重新查看 `API - Props` 的部分**
+- `px` 全面调整为 `rpx` 单位,多个`Props` 的参数相应调整,请查看文档
+ - 新增 `pixelRatio` 参数,该参数为设计稿的设备像素比基准值,默认为 `2` 倍素
+## 1.7.18(2021-09-05)
+本次更新:
+- 修复一个已知问题
+## 1.7.17(2021-08-23)
+本次更新:
+- 更新示例项目
+## 1.7.16(2021-08-14)
+本次更新:
+- 更新示例项目
+## 1.7.15(2021-08-03)
+本次更新:
+- 新增文字竖向展示的功能,详见文档说明
+## 1.7.13(2021-08-02)
+本次更新:
+- 更新文档
+## 1.7.12(2021-07-30)
+本次更新:
+- 修复示例项目的已知问题
+- 现已提供Almost-Lottery抽奖转盘的uniCloud云端一体页面模板
+- 现已提供Almost-Lottery抽奖转盘云端一体页面配套的Admin配置中心
+## 1.7.11(2021-07-22)
+本次更新:
+- 修复部分安卓手机文字大小异常的问题
+- 字段 `strHeightMultiple` 更换为 `strLineHeight`
+## 1.7.10(2021-07-12)
+本次更新:
+- 修复奖品名称 `name` 为空字符串时无法成功绘制转盘的问题
+- 新增 `prizeNameDrawed` 是否绘制奖品名称的配置项,现在可以仅展示奖品图片了
+## 1.7.9(2021-07-09)
+本次更新:
+- 优化组件内部代码
+- 修复奖品图片已然是 `base64` 格式时导致转盘绘制失败的问题
+- 文档新增QQ群号,让沟通更便捷
+## 1.7.8(2021-07-08)
+本次更新:
+- 调整 `Canvas` 默认宽高为 `280`
+## 1.7.7(2021-07-08)
+本次更新:
+- 新增多个配置项,满足更多自定义需求
+- 优化多行文本情况下非中文字符的字节处理
+- 修复偶发的第一条数据文本不居中显示的问题
+## 1.7.6(2021-07-02)
+本次更新:
+- 调整 `imageWidth` 、 `imageHeight` 字段为 `imgWidth` 、 `imgHeight` 
+- 更新示例项目
+## 1.7.5(2021-07-01)
+本次更新:
+- 新增配置项 `imgMarginStr` 奖品图片距离奖品文字的距离
+## 1.7.4(2021-06-28)
+本次更新:
+- 新增轮盘旋转或指针旋转配置项
+- 转盘内置的外环图片以及按钮图片统一调整为 `image` 展示
+- 更新相关文档说明
+## 1.7.3(2021-06-16)
+本次更新:
+- 优化错误提示
+- 优化示例项目
+- 优化文档说明
+## 1.7.2(2021-06-11)
+本次更新:
+- 新增 `canvasId` 参数配置项,多画板情况下需要配置不同的 `canvasId`
+- 优化多画板情况下的缓存功能
+- 优化示例项目
+- 修改文档说明
+## 1.7.1(2021-06-10)
+本次更新:
+- 优化示例项目中的注释
+## 1.7.0(2021-06-04)
+本次更新:
+- 修复 `1.6.1` 引起的多行奖品文字行高异常的问题
+- 新增配置转盘外环和抽奖按钮图片的功能,详见文档说明
+- 更新示例项目,新增抽奖次数等业务有关的逻辑供参考
+## 1.6.1(2021-05-28)
+本次更新:
+- 修复小程序平台画板模糊的问题
+## 1.6.0(2021-05-28)
+本次更新:
+- 新增奖品区块是否开启描边的配置项,默认不开启
+- 调整画板缓存为默认不开启
+- 优化代码
+- 优化文档说明
+- 更新示例项目并修改部分注释
+## 1.5.13(2021-05-22)
+本次更新:
+- 优化文档说明
+- 更新示例项目
+## 1.5.12(2021-05-22)
+本次更新:
+- 新增配置项 `strokeColor` 奖品区块边框颜色
+- 更新文档说明
+## 1.5.11(2021-05-19)
+本次更新:
+- 新增`strMarginOutside`参数,用于设置奖品文字距离边缘的距离
+- 修复奖品文字在某些情况下不是居中显示的问题
+## 1.5.10(2021-05-19)
+本次更新:
+- 修复示例项目中权重值相同时的取值逻辑
+## 1.5.9(2021-05-14)
+本次更新:
+- 调整代码,优化小程序端的展示
+## 1.5.8(2021-05-12)
+本次更新:
+- 文档增加预警提示:不再维护非 `uni_modules` 模式下的版本
+## 1.5.7(2021-05-12)
+本次更新:
+- 修复小程序平台奖品名称不清晰的问题
+## 1.5.6(2021-03-18)
+本次更新:
+- 适配 uni_modules 插件模式

+ 1087 - 0
uni_modules/almost-lottery/components/almost-lottery/almost-lottery.vue

@@ -0,0 +1,1087 @@
+<template>
+  <view class="almost-lottery">
+    <view class="almost-lottery__wrap" :style="{ width: lotterySize + 'rpx', height: lotterySize + 'rpx' }">
+      <view class="lottery-action" :style="{ width: actionSize + 'rpx', height: actionSize + 'rpx', left: canvasMarginOutside + 'rpx' }"></view>
+      <view class="str-margin-outside" :style="{ left: strMarginOutside + 'rpx' }"></view>
+      <view class="img-margin-str" :style="{ left: imgMarginStr + 'rpx' }"></view>
+      <view class="img-size" :style="{ width: imgWidth + 'rpx', height: imgHeight + 'rpx' }"></view>
+      <template v-if="lotteryImg">
+        <image
+          class="almost-lottery__bg"
+          mode="widthFix"
+          :src="lotteryBg"
+          :style="{
+            width: lotteryPxSize + 'px',
+            height: lotteryPxSize + 'px'
+          }"
+        ></image>
+        <image
+          :class="[
+            'almost-lottery__canvas-img',
+            { 'almost-lottery__canvas-img-other': !selfRotaty },
+            { 'almost-lottery__canvas-img-self': selfRotated }
+          ]"
+          mode="widthFix"
+          :src="lotteryImg"
+          :style="{
+            width: canvasImgPxSize + 'px',
+            height: canvasImgPxSize  + 'px',
+            left: canvasImgToLeftPx + 'px',
+            top: canvasImgToLeftPx + 'px',
+            transform: `rotate(${canvasAngle + targetAngle}deg)`,
+            transitionDuration: `${transitionDuration}s`
+          }"
+        ></image>
+        <image
+          class="almost-lottery__action-bg"
+          mode="widthFix"
+          :src="actionBg"
+          :style="{
+            width: actionPxSize + 'px',
+            height: actionPxSize + 'px',
+            left: actionBgToLeftPx + 'px',
+            top: actionBgToLeftPx + 'px',
+            transform: `rotate(${actionAngle + targetActionAngle}deg)`,
+            transitionDuration: `${transitionDuration}s`
+          }"
+          @click="handleActionStart"
+        ></image>
+      </template>
+    </view>
+    
+    <!-- 为了兼容 app 端 ctx.measureText 所需的标签 -->
+    <text class="almost-lottery__measureText" :style="{ fontSize: higtFontSize + 'px' }">{{ measureText }}</text>
+    
+    <!-- #ifdef MP-ALIPAY -->
+    <canvas 
+      :class="className"
+      :id="canvasId"
+      :width="higtCanvasSize"
+      :height="higtCanvasSize"
+      :style="{
+        width: higtCanvasSize + 'px',
+        height: higtCanvasSize + 'px'
+      }"
+    />
+    <!-- #endif -->
+    <!-- #ifndef MP-ALIPAY -->
+    <canvas
+      :class="className"
+      :canvas-id="canvasId"
+      :width="higtCanvasSize"
+      :height="higtCanvasSize"
+      :style="{
+        width: higtCanvasSize + 'px',
+        height: higtCanvasSize + 'px'
+      }"
+    />
+    <!-- #endif -->
+  </view>
+</template>
+
+<script>
+	import { getStore, setStore, clearStore, circleImg, clacTextLen, downloadFile, pathToBase64, base64ToPath } from '@/uni_modules/almost-lottery/utils/almost-utils.js'
+  export default {
+    name: 'AlmostLottery',
+    props: {
+      // 设计稿的像素比基准值
+      pixelRatio: {
+        type: Number,
+        default: 2
+      },
+      // canvas 标识
+      canvasId: {
+        type: String,
+        default: 'almostLottery'
+      },
+      // 渲染延迟
+      renderDelay: {
+        type: Number,
+        default: 0
+      },
+      // 抽奖转盘的整体尺寸
+      lotterySize: {
+        type: Number,
+        default: 600
+      },
+      // 抽奖按钮的尺寸
+      actionSize: {
+        type: Number,
+        default: 200
+      },
+			// canvas边缘距离转盘边缘的距离
+			canvasMarginOutside: {
+        type: Number,
+        default: 90
+      },
+      // 奖品列表
+      prizeList: {
+        type: Array,
+        required: true,
+        validator: (value) => {
+          return value.length > 1
+        }
+      },
+      // 中奖奖品在列表中的下标
+      prizeIndex: {
+        type: Number,
+        required: true
+      },
+      // 奖品区块对应背景颜色
+      colors: {
+        type: Array,
+        default: () => [
+          '#FFFFFF',
+          '#FFBF05'
+        ]
+      },
+      // 转盘外环背景图
+      lotteryBg: {
+        type: String,
+        default: '/uni_modules/almost-lottery/static/almost-lottery/almost-lottery__bg2x.png'
+      },
+      // 抽奖按钮背景图
+      actionBg: {
+        type: String,
+        default: '/uni_modules/almost-lottery/static/almost-lottery/almost-lottery__action2x.png'
+      },
+      // 是否绘制奖品名称
+      prizeNameDrawed: {
+				type: Boolean,
+				default: true
+      },
+      // 是否开启奖品区块描边
+      stroked: {
+				type: Boolean,
+				default: false
+			},
+      // 描边颜色
+      strokeColor: {
+        type: String,
+        default: '#FFBF05'
+      },
+      // 旋转的类型
+      rotateType: {
+        type: String,
+        default: 'roulette'
+      },
+      // 是否开启自转
+      selfRotaty: {
+				type: Boolean,
+				default: false
+			},
+      // 自转时,最少转多少毫秒
+      selfTime: {
+				type: Number,
+				default: 1000
+      },
+      // 旋转动画时间 单位s
+      duration: {
+        type: Number,
+        default: 8
+      },
+      // 旋转的圈数
+      ringCount: {
+        type: Number,
+        default: 8
+      },
+      // 指针位置
+      pointerPosition: {
+        type: String,
+        default: 'edge',
+        validator: (value) => {
+          return value === 'edge' || value === 'middle'
+        }
+      },
+      // 文字方向
+      strDirection: {
+        type: String,
+        default: 'horizontal',
+        validator: (value) => {
+          return value === 'horizontal' || value === 'vertical'
+        }
+      },
+      // 字体颜色
+      strFontColors: {
+        type: Array,
+        default: () => [
+          '#FFBF05',
+          '#FFFFFF'
+        ]
+      },
+      // 文字的大小
+      strFontSize: {
+        type: Number,
+        default: 24
+      },
+      // 奖品文字距离边缘的距离
+      strMarginOutside: {
+        type: Number,
+        default: 0
+      },
+      // 奖品图片距离奖品文字的距离
+      imgMarginStr: {
+        type: Number,
+        default: 60
+      },
+      // 奖品文字多行情况下的行高
+      strLineHeight: {
+        type: Number,
+        default: 1.2
+      },
+      // 奖品文字总长度限制
+      strMaxLen: {
+        type: Number,
+        default: 12
+      },
+      // 奖品文字多行情况下第一行文字长度
+      strLineLen: {
+        type: Number,
+        default: 6
+      },
+      // 奖品图片的宽
+      imgWidth: {
+        type: Number,
+        default: 50
+      },
+      // 奖品图片的高
+      imgHeight: {
+        type: Number,
+        default: 50
+      },
+			// 是否绘制奖品图片
+			imgDrawed: {
+				type: Boolean,
+				default: true
+			},
+      // 奖品图片是否裁切为圆形
+      imgCircled: {
+      	type: Boolean,
+      	default: false
+      },
+			// 转盘绘制成功的提示
+			successMsg: {
+				type: String,
+				default: '奖品准备就绪,快来参与抽奖吧'
+			},
+			// 转盘绘制失败的提示
+			failMsg: {
+				type: String,
+				default: '奖品仍在准备中,请稍后再来...'
+			},
+			// 是否开启画板的缓存
+			canvasCached: {
+				type: Boolean,
+				default: false
+			}
+    },
+    data() {
+      return {
+        // 画板className
+        className: 'almost-lottery__canvas',
+        // 高清固定 2 倍,不再从 system 中动态获取,因为 h5、app-vue 中单个尺寸过大时存在 iOS/Safari 无法绘制的问题,且 2 倍基本也可以解决模糊的问题
+        systemPixelRatio: 2,
+        // 抽奖转盘的整体px尺寸
+        lotteryPxSize: 0,
+        // 画板的px尺寸
+        canvasImgPxSize: 0,
+        // 抽奖按钮的px尺寸
+        actionPxSize: 0,
+        // 奖品文字距离转盘边缘的距离
+        strMarginPxOutside: 0,
+        // 奖品图片相对奖品文字的距离
+        imgMarginPxStr: 0,
+        // 奖品图片的宽、高
+        imgPxWidth: 0,
+        imgPxHeight: 0,
+        // 画板导出的图片
+        lotteryImg: '',
+        // 旋转到奖品目标需要的角度
+        targetAngle: 0,
+        targetActionAngle: 0,
+        // 配合自转使用
+        selfRotated: false,
+        selfRotatyStartTime: null,
+        // 是否正在旋转
+        isRotate: false,
+        // 当前停留在那个奖品的序号
+        stayIndex: 0,
+        // 当前中奖奖品的序号
+        targetIndex: 0,
+				// 是否存在可用的缓存转盘图
+				isCacheImg: false,
+				oldLotteryImg: '',
+        // 解决 app 不支持 measureText 的问题
+				// app 已在 2.9.3 的版本中提供了对 measureText 的支持,将在后续版本逐渐稳定后移除相关兼容代码
+        measureText: ''
+      }
+    },
+    computed: {
+      // 高清尺寸
+      higtCanvasSize() {
+        return this.canvasImgPxSize * this.systemPixelRatio
+      },
+      // 高清字体
+      higtFontSize() {
+        return Math.round(this.strFontSize / this.pixelRatio) * this.systemPixelRatio
+      },
+      // 高清行高
+      higtHeightMultiple() {
+        return Math.round(this.strFontSize / this.pixelRatio) * this.strLineHeight * this.systemPixelRatio
+      },
+      canvasImgToLeftPx () {
+        return (this.lotteryPxSize - this.canvasImgPxSize) / 2
+      },
+      actionBgToLeftPx () {
+        return (this.lotteryPxSize - this.actionPxSize) / 2
+      },
+      // 根据奖品列表计算 canvas 旋转角度
+      canvasAngle() {
+        let result = 0
+        
+        let prizeCount = this.prizeList.length
+        let prizeClip = 360 / prizeCount
+        let diffNum = 90 / prizeClip
+        if (this.pointerPosition === 'edge' || this.rotateType === 'pointer') {
+          result = -(prizeClip * diffNum)
+        } else {
+          result = -(prizeClip * diffNum + prizeClip / 2)
+        }
+        return result
+      },
+      actionAngle() {
+        return 0
+      },
+      // 外圆的半径
+      outsideRadius() {
+        return this.higtCanvasSize / 2
+      },
+      // 内圆的半径
+      insideRadius() {
+        return 20 * this.systemPixelRatio
+      },
+      // 文字距离边缘的距离
+      textRadius() {
+        return this.strMarginPxOutside * this.systemPixelRatio || (this.higtFontSize / 2)
+      },
+      // 根据画板的宽度计算奖品文字与中心点的距离
+      textDistance() {
+        const textZeroY = Math.round(this.outsideRadius - (this.insideRadius / 2))
+        return textZeroY - this.textRadius
+      },
+      // 旋转动画时间 单位 s
+      transitionDuration () {
+        return this.selfRotaty ? 2 : this.duration
+      }
+    },
+    watch: {
+      // 监听获奖序号的变动
+      prizeIndex(newVal, oldVal) {
+        if (newVal > -1) {
+          if (this.selfRotaty) {
+            const diffTime = Date.now() - this.selfRotatyStartTime
+            const timeDelay = diffTime < this.selfTime ? this.selfTime : 0
+            setTimeout(() => {
+              this.selfRotated = false
+              this.targetIndex = newVal
+              this.onRotateStart()
+            }, timeDelay)
+          } else {
+            setTimeout(() => {
+              this.targetIndex = newVal
+              this.onRotateStart()
+            }, 0)
+          }
+        } else {
+          console.info('旋转结束,prizeIndex 已重置')
+        }
+      }
+    },
+    methods: {
+      // 开始旋转
+      onRotateStart() {
+        // 奖品总数
+        if (!this.selfRotaty) {
+          if (this.isRotate) return
+          this.isRotate = true
+        }
+        
+        let prizeCount = this.prizeList.length
+        let baseAngle = 360 / prizeCount
+        let angles = 0
+        
+        let ringCount = this.selfRotaty ? 1 : this.ringCount
+        
+        if (this.rotateType === 'pointer') {
+          if (this.targetActionAngle === 0) {
+            // 第一次旋转
+            angles = (this.targetIndex - this.stayIndex) * baseAngle + baseAngle / 2 - this.actionAngle
+          } else {
+            // 后续旋转
+            // 后续继续旋转 就只需要计算停留的位置与目标位置的角度
+            angles = (this.targetIndex - this.stayIndex) * baseAngle
+          }
+          
+          // 更新目前序号
+          this.stayIndex = this.targetIndex
+          // 转 8 圈,圈数越多,转的越快
+          this.targetActionAngle += angles + 360 * ringCount
+          // console.log('targetActionAngle', this.targetActionAngle)
+        } else {
+          if (this.targetAngle === 0) {
+            // 第一次旋转
+            // 因为第一个奖品是从0°开始的,即水平向右方向
+            // 第一次旋转角度 = 270度 - (停留的序号-目标序号) * 每个奖品区间角度 - 每个奖品区间角度的一半 - canvas自身旋转的度数
+            angles = (270 - (this.targetIndex - this.stayIndex) * baseAngle - baseAngle / 2) - this.canvasAngle
+          } else {
+            // 后续旋转
+            // 后续继续旋转 就只需要计算停留的位置与目标位置的角度
+            angles = -(this.targetIndex - this.stayIndex) * baseAngle
+          }
+          
+          // 更新目前序号
+          this.stayIndex = this.targetIndex
+          // 转 8 圈,圈数越多,转的越快
+          this.targetAngle += angles + 360 * ringCount
+        }
+
+        // 计算转盘结束的时间,预加一些延迟确保转盘停止后触发结束事件
+        let endTime = this.selfRotaty ? 0 : (this.transitionDuration * 1000 + 100)
+        let endTimer = setTimeout(() => {
+          clearTimeout(endTimer)
+          endTimer = null
+
+          this.isRotate = false
+          this.$emit('draw-end')
+        }, endTime)
+
+        let resetPrizeTimer = setTimeout(() => {
+          clearTimeout(resetPrizeTimer)
+          resetPrizeTimer = null
+
+          // 每次抽奖结束后都要重置父级组件的 prizeIndex
+          this.$emit('reset-index')
+        }, endTime + 50)
+      },
+      // 点击 开始抽奖 按钮
+      handleActionStart() {
+        if (!this.lotteryImg) return
+        if (this.isRotate) return
+        
+        this.$emit('draw-before', (shouldContinue) => {
+          console.log('shouldContinue', shouldContinue)
+          if (!shouldContinue) return
+          
+          const ringDuration = (this.duration / this.ringCount).toFixed(1)
+          if (ringDuration >= 2.5) {
+            console.warn('当前每一圈的旋转可能过慢,请检查 duration 和 ringCount 这 2 个参数是否设置合理')
+          } else if (ringDuration < 1) {
+            console.warn('当前每一圈的旋转可能过快,请检查 duration 和 ringCount 这 2 个参数是否设置合理')
+          }
+          
+          if (this.selfRotaty) {
+            this.isRotate = true
+            this.selfRotated = true
+            this.selfRotatyStartTime = Date.now()
+          }
+          
+          this.$emit('draw-start')
+        })
+      },
+      // 渲染转盘
+      async onCreateCanvas() {
+        // 获取 canvas 画布
+        const canvasId = this.canvasId
+        const ctx = uni.createCanvasContext(canvasId, this)
+
+        // canvas 的宽高
+        let canvasW = this.higtCanvasSize
+        let canvasH = this.higtCanvasSize
+
+        // 根据奖品个数计算 角度
+        let prizeCount = this.prizeList.length
+        let baseAngle = Math.PI * 2 / prizeCount
+
+        // 设置字体
+        ctx.setFontSize(this.higtFontSize)
+
+        // 注意,开始画的位置是从0°角的位置开始画的。也就是水平向右的方向。
+        // 画具体内容
+        for (let i = 0; i < prizeCount; i++) {
+					let prizeItem = this.prizeList[i]
+          // 当前角度
+          let angle = i * baseAngle
+
+          // 保存当前画布的状态
+          ctx.save()
+          
+          // x => 圆弧对应的圆心横坐标 x
+          // y => 圆弧对应的圆心横坐标 y
+          // radius => 圆弧的半径大小
+          // startAngle => 圆弧开始的角度,单位是弧度
+          // endAngle => 圆弧结束的角度,单位是弧度
+          // anticlockwise(可选) => 绘制方向,true 为逆时针,false 为顺时针
+          
+          ctx.beginPath()
+          // 外圆
+          ctx.arc(canvasW * 0.5, canvasH * 0.5, this.outsideRadius, angle, angle + baseAngle, false)
+          // 内圆
+          ctx.arc(canvasW * 0.5, canvasH * 0.5, this.insideRadius, angle + baseAngle, angle, true)
+          
+          // 每个奖品区块背景填充颜色
+          if (this.colors.length === 2) {
+            ctx.setFillStyle(this.colors[i % 2])
+          } else {
+            ctx.setFillStyle(this.colors[i])
+          }
+          // 填充颜色
+          ctx.fill()
+          
+          // 开启描边
+          if (this.stroked) {
+            // 设置描边颜色
+            ctx.setStrokeStyle(`${this.strokeColor}`)
+            // 描边
+            ctx.stroke()
+          }
+
+          // 开始绘制奖品内容
+          // 重新映射画布上的 (0,0) 位置
+          let translateX = canvasW * 0.5 + Math.cos(angle + baseAngle / 2) * this.textDistance
+          let translateY = canvasH * 0.5 + Math.sin(angle + baseAngle / 2) * this.textDistance
+          ctx.translate(translateX, translateY)
+
+          // 绘制奖品名称
+          let rewardName = this.strLimit(prizeItem.prizeName)
+          
+          // 设置文字颜色
+          if (this.strFontColors.length === 1) {
+            ctx.setFillStyle(this.strFontColors[0])
+          } else if (this.strFontColors.length === 2) {
+            ctx.setFillStyle(this.strFontColors[i % 2])
+          } else {
+            ctx.setFillStyle(this.strFontColors[i])
+          }
+          
+          // rotate方法旋转当前的绘图,因为文字是和当前扇形中心线垂直的
+          ctx.rotate(angle + (baseAngle / 2) + (Math.PI / 2))
+
+          // 设置文本位置并处理换行
+          if (this.strDirection === 'horizontal') {
+            // 是否需要换行
+            if (rewardName && this.prizeNameDrawed) {
+              let realLen = clacTextLen(rewardName).realLen
+              let isLineBreak = realLen > this.strLineLen
+              if (isLineBreak) {
+                // 获得多行文本数组
+                let textCount = 0
+                let tempTxt = ''
+                let rewardNames = []
+                for (let j = 0; j < rewardName.length; j++) {
+                  textCount += clacTextLen(rewardName[j]).byteLen
+                  tempTxt += rewardName[j]
+                  
+                  if (textCount >= (this.strLineLen * 2)) {
+                    rewardNames.push(tempTxt)
+                    textCount = 0
+                    tempTxt = ''
+                  } else {
+                    if ((rewardName.length - 1) === j) {
+                      rewardNames.push(tempTxt)
+                      textCount = 0
+                      tempTxt = ''
+                    }
+                  }
+                }
+                
+                // 循环文本数组,计算每一行的文本宽度
+                for (let j = 0; j < rewardNames.length; j++) {
+                  if (ctx.measureText && ctx.measureText(rewardNames[j]).width > 0) {
+                    // 文本的宽度信息
+                    let tempStrSize = ctx.measureText(rewardNames[j])
+                    let tempStrWidth = -(tempStrSize.width / 2).toFixed(2)
+                    ctx.fillText(rewardNames[j], tempStrWidth, j * this.higtHeightMultiple)
+                  } else {
+                    this.measureText = rewardNames[j]
+                    
+                    // 等待页面重新渲染
+                    await this.$nextTick()
+                    
+                    let textWidth = await this.getTextWidth()
+                    let tempStrWidth = -(textWidth / 2).toFixed(2)
+                    ctx.fillText(rewardNames[j], tempStrWidth, j * this.higtHeightMultiple)
+                    // console.log(rewardNames[j], textWidth, j)
+                  }
+                }
+              } else {
+                if (ctx.measureText && ctx.measureText(rewardName).width > 0) {
+                  // 文本的宽度信息
+                  let tempStrSize = ctx.measureText(rewardName)
+                  let tempStrWidth = -(tempStrSize.width / 2).toFixed(2)
+                  ctx.fillText(rewardName, tempStrWidth, 0)
+                } else {
+                  this.measureText = rewardName
+                  
+                  // 等待页面重新渲染
+                  await this.$nextTick()
+                  
+                  let textWidth = await this.getTextWidth()
+                  let tempStrWidth = -(textWidth / 2).toFixed(2)
+                  ctx.fillText(rewardName, tempStrWidth, 0)
+                }
+              }
+            }
+          } else {
+            let rewardNames = rewardName.split('')
+            for (let j = 0; j < rewardNames.length; j++) {
+              if (ctx.measureText && ctx.measureText(rewardNames[j]).width > 0) {
+                // 文本的宽度信息
+                let tempStrSize = ctx.measureText(rewardNames[j])
+                let tempStrWidth = -(tempStrSize.width / 2).toFixed(2)
+                ctx.fillText(rewardNames[j], tempStrWidth, j * this.higtHeightMultiple)
+              } else {
+                this.measureText = rewardNames[j]
+                
+                // 等待页面重新渲染
+                await this.$nextTick()
+                
+                let textWidth = await this.getTextWidth()
+                let tempStrWidth = -(textWidth / 2).toFixed(2)
+                ctx.fillText(rewardNames[j], tempStrWidth, j * this.higtHeightMultiple)
+                // console.log(rewardNames[j], textWidth, i)
+              }
+            }
+          }
+          
+
+          // 绘制奖品图片,文字竖向展示时,不支持图片展示
+          if (this.imgDrawed && prizeItem.prizeImage && this.strDirection !== 'vertical') {
+						// App-Android平台 系统 webview 更新到 Chrome84+ 后 canvas 组件绘制本地图像 uni.canvasToTempFilePath 会报错
+						// 统一将图片处理成 base64
+						// https://ask.dcloud.net.cn/question/103303
+						let reg = /^(https|http)/g
+						// 处理远程图片
+						if (reg.test(prizeItem.prizeImage)) {
+              let platformTips = ''
+              // #ifdef APP-PLUS
+							platformTips = ''
+              // #endif
+              // #ifdef MP
+							platformTips = '需要处理好下载域名的白名单问题,'
+              // #endif
+              // #ifdef H5
+							platformTips = '需要处理好跨域问题,'
+              // #endif
+							console.warn(`###当前数据列表中的奖品图片为网络图片,${platformTips}开始尝试下载图片...###`)
+							let res = await downloadFile(prizeItem.prizeImage)
+							console.log('处理远程图片', res)
+							if (res.ok) {
+								let tempFilePath = res.tempFilePath
+								// #ifndef MP
+								prizeItem.prizeImage = await pathToBase64(tempFilePath)
+								// #endif
+								// #ifdef MP
+								prizeItem.prizeImage = tempFilePath
+								// #endif
+							} else {
+                this.handlePrizeImgSuc({
+                  ok: false,
+                  data: res.data,
+                  msg: res.msg
+                })
+              }
+						} else {
+							// #ifndef MP
+              // 不是小程序环境,把本地图片处理成 base64
+              if (prizeItem.prizeImage.indexOf(';base64,') === -1) {
+                console.log('开始处理本地图片', prizeItem.prizeImage)
+                prizeItem.prizeImage = await pathToBase64(prizeItem.prizeImage)
+                console.log('处理本地图片结束', prizeItem.prizeImage)
+              }
+							// #endif
+              
+              // #ifdef MP-WEIXIN
+              // 小程序环境,把 base64 处理成小程序的本地临时路径
+              if (prizeItem.prizeImage.indexOf(';base64,') !== -1) {
+                console.log('开始处理BASE64图片', prizeItem.prizeImage)
+                prizeItem.prizeImage = await base64ToPath(prizeItem.prizeImage)
+                console.log('处理BASE64图片完成', prizeItem.prizeImage)
+              }
+              // #endif
+						}
+            
+            let prizeImageX = -(this.imgPxWidth * this.systemPixelRatio / 2)
+            let prizeImageY = this.imgMarginPxStr * this.systemPixelRatio
+            let prizeImageW = this.imgPxWidth * this.systemPixelRatio
+            let prizeImageH = this.imgPxHeight * this.systemPixelRatio
+            if (this.imgCircled) {
+              // 重新设置每个圆形的背景色
+              if (this.colors.length === 2) {
+                ctx.setFillStyle(this.colors[i % 2])
+              } else {
+                ctx.setFillStyle(this.colors[i])
+              }
+              circleImg(ctx, prizeItem.prizeImage, prizeImageX, prizeImageY, prizeImageW, prizeImageH)
+            } else {
+              ctx.drawImage(prizeItem.prizeImage, prizeImageX, prizeImageY, prizeImageW, prizeImageH)
+            }
+          }
+
+          ctx.restore()
+        }
+
+        // 保存绘图并导出图片
+        ctx.draw(true, () => {
+          let drawTimer = setTimeout(() => {
+            clearTimeout(drawTimer)
+            drawTimer = null
+
+            // #ifdef MP-ALIPAY
+            ctx.toTempFilePath({
+              destWidth: this.higtCanvasSize,
+              destHeight: this.higtCanvasSize,
+              success: (res) => {
+                // console.log(res.apFilePath)
+                this.handlePrizeImg({
+									ok: true,
+									data: res.apFilePath,
+									msg: '画布导出生成图片成功'
+								})
+              },
+							fail: (err) => {
+                this.handlePrizeImg({
+									ok: false,
+									data: err,
+									msg: '画布导出生成图片失败'
+								})
+							}
+            })
+            // #endif
+            
+            // #ifndef MP-ALIPAY
+            uni.canvasToTempFilePath({
+              canvasId: this.canvasId,
+              destWidth: this.higtCanvasSize,
+              destHeight: this.higtCanvasSize,
+              success: (res) => {
+                // 在 H5 平台下,tempFilePath 为 base64
+                // console.log(res.tempFilePath)
+                this.handlePrizeImg({
+									ok: true,
+									data: res.tempFilePath,
+									msg: '画布导出生成图片成功'
+								})
+              },
+							fail: (err) => {
+                this.handlePrizeImg({
+									ok: false,
+									data: err,
+									msg: '画布导出生成图片失败'
+								})
+							}
+            }, this)
+            // #endif
+          }, 500)
+        })
+      },
+      // 处理导出的图片
+      handlePrizeImg(res) {
+				if (res.ok) {
+					let data = res.data
+					
+					if (!this.canvasCached) {
+						this.lotteryImg = data
+						this.handlePrizeImgSuc(res)
+						return
+					}
+					
+					// #ifndef H5
+					if (this.isCacheImg) {
+						uni.getSavedFileList({
+							success: (sucRes) => {
+								let fileList = sucRes.fileList
+								// console.log('getSavedFileList Cached', fileList)
+								
+								let cached = false
+                
+                if (fileList.length) {
+                  for (let i = 0; i < fileList.length; i++) {
+                  	let item = fileList[i]
+                  	if (item.filePath === data) {
+                  		cached = true
+                  		this.lotteryImg = data
+                  		
+                  		console.info('经查,本地缓存中存在的转盘图可用,本次将不再绘制转盘')
+                  		this.handlePrizeImgSuc(res)
+                  		break
+                  	}
+                  }
+                }
+								
+								if (!cached) {
+									console.info('经查,本地缓存中存在的转盘图不可用,需要重新初始化转盘绘制')
+									this.initCanvasDraw()
+								}
+							},
+							fail: (err) => {
+								this.initCanvasDraw()
+							}
+						})
+					} else {
+						uni.saveFile({
+							tempFilePath: data,
+							success: (sucRes) => {
+								let filePath = sucRes.savedFilePath
+								// console.log('saveFile', filePath)
+								setStore(`${this.canvasId}LotteryImg`, filePath)
+								this.lotteryImg = filePath
+								this.handlePrizeImgSuc({
+									ok: true,
+									data: filePath,
+									msg: '画布导出生成图片成功'
+								})
+							},
+							fail: (err) => {
+								this.handlePrizeImg({
+									ok: false,
+									data: err,
+									msg: '画布导出生成图片失败'
+								})
+							}
+						})
+					}
+					// #endif
+					// #ifdef H5
+					setStore(`${this.canvasId}LotteryImg`, data)
+					this.lotteryImg = data
+					this.handlePrizeImgSuc(res)
+          
+          // console info
+          let consoleText = this.isCacheImg ? '缓存' : '导出'
+          console.info(`当前为 H5 端,使用${consoleText}中的 base64 图`)
+					// #endif
+				} else {
+          console.error(res.msg, res)
+					// #ifdef H5
+					console.error('###当前为 H5 端,下载网络图片需要后端配置允许跨域###')
+					// #endif
+					// #ifdef MP
+					console.error('###当前为小程序端,下载网络图片需要配置域名白名单###')
+					// #endif
+				}
+      },
+			// 处理图片完成
+			handlePrizeImgSuc (res) {
+				this.$emit('finish', {
+					ok: res.ok,
+					data: res.data,
+					msg: res.ok ? this.successMsg : this.failMsg
+				})
+			},
+      // 兼容 app 端不支持 ctx.measureText
+      // 已知问题:初始绘制时,低端安卓机 平均耗时 2s
+      // hbx 2.8.12+ 已在 app 端支持
+      getTextWidth() {
+        console.warn('正在采用兼容方式获取文本的 size 信息')
+        let query = uni.createSelectorQuery().in(this)
+        let nodesRef = query.select('.almost-lottery__measureText')
+        return new Promise((resolve, reject) => {
+          nodesRef.fields({
+            size: true,
+          }, (res) => {
+            resolve(res.width)
+          }).exec()
+        })
+      },
+      // 处理文字溢出
+      strLimit(value) {
+        let maxLength = this.strMaxLen
+        if (!value || !maxLength) return value
+        return clacTextLen(value).realLen > maxLength ? value.slice(0, maxLength - 1) + '..' : value
+      },
+			// 检查本地缓存中是否存在转盘图
+			checkCacheImg () {
+				console.log('检查本地缓存中是否存在转盘图')
+				// 检查是否已有缓存的转盘图
+				// 检查是否与本次奖品数据相同
+				this.oldLotteryImg = getStore(`${this.canvasId}LotteryImg`)
+				let oldPrizeList = getStore(`${this.canvasId}PrizeList`)
+				let newPrizeList = JSON.stringify(this.prizeList)
+				if (this.oldLotteryImg) {
+          console.log(`经查,本地缓存中存在转盘图 => ${this.oldLotteryImg},继续判断这张缓存图是否可用`)
+					if (oldPrizeList === newPrizeList) {
+						this.isCacheImg = true
+						
+						console.log('缓存图可用')
+						this.handlePrizeImg({
+							ok: true,
+							data: this.oldLotteryImg,
+							msg: '画布导出生成图片成功'
+						})
+						return
+					}
+				}
+				
+				this.initCanvasDraw()
+			},
+      // 初始化绘制
+      initCanvasDraw () {
+				console.log('开始初始化转盘绘制')
+				this.isCacheImg = false
+				this.lotteryImg = ''
+				clearStore(`${this.canvasId}LotteryImg`)
+        setStore(`${this.canvasId}PrizeList`, this.prizeList)
+        this.onCreateCanvas()
+      },
+      // 预处理初始化
+      async beforeInit () {
+        let query = uni.createSelectorQuery().in(this)
+        // 处理 rpx 自适应尺寸
+        let lotterySize = await new Promise((resolve) => {
+          query.select('.almost-lottery__wrap').boundingClientRect((rects) => {
+            resolve(rects)
+            // console.log('处理 lottery rpx 的自适应', rects)
+          }).exec()
+        })
+        let actionSize = await new Promise((resolve) => {
+          query.select('.lottery-action').boundingClientRect((rects) => {
+            resolve(rects)
+            // console.log('处理 action rpx 的自适应', rects)
+          }).exec()
+        })
+        let strMarginSize = await new Promise((resolve) => {
+          query.select('.str-margin-outside').boundingClientRect((rects) => {
+            resolve(rects)
+            // console.log('处理 str-margin-outside rpx 的自适应', rects)
+          }).exec()
+        })
+        let imgMarginStr = await new Promise((resolve) => {
+          query.select('.img-margin-str').boundingClientRect((rects) => {
+            resolve(rects)
+            // console.log('处理 img-margin-str rpx 的自适应', rects)
+          }).exec()
+        })
+        let imgSize = await new Promise((resolve) => {
+          query.select('.img-size').boundingClientRect((rects) => {
+            resolve(rects)
+            // console.log('处理 img-size rpx 的自适应', rects)
+          }).exec()
+        })
+        
+        this.lotteryPxSize = Math.floor(lotterySize.width)
+        this.canvasImgPxSize = this.lotteryPxSize - Math.floor(actionSize.left) + Math.floor(lotterySize.left)
+        this.actionPxSize = Math.floor(actionSize.width)
+        
+        this.strMarginPxOutside = Math.floor(strMarginSize.left) - Math.floor(lotterySize.left)
+        this.imgMarginPxStr = Math.floor(imgMarginStr.left) - Math.floor(lotterySize.left)
+        this.imgPxWidth = Math.floor(imgSize.width)
+        this.imgPxHeight = Math.floor(imgSize.height)
+        
+        // console.log(this.lotteryPxSize, this.canvasImgPxSize, this.actionPxSize)
+        
+        let stoTimer = setTimeout(() => {
+          clearTimeout(stoTimer)
+          stoTimer = null
+          
+          // 判断画板是否设置缓存
+          if (this.canvasCached) {
+          	this.checkCacheImg()
+          } else {
+          	this.initCanvasDraw()
+          }
+        }, 50)
+      }
+    },
+    mounted() {
+      this.$nextTick(() => {
+        let delay = 50 + this.renderDelay
+        
+        let stoTimer = setTimeout(() => {
+          clearTimeout(stoTimer)
+          stoTimer = null
+          
+          this.beforeInit()
+        }, delay)
+      })
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .almost-lottery {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+  }
+  
+  // 以下元素不可见,是 canvas 的实例
+  .almost-lottery__canvas {
+    position: absolute;
+    left: -9999px;
+    opacity: 0;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+  }
+  
+  // 以下元素不可见,用于获得自适应的值
+  .lottery-action,
+  .str-margin-outside,
+  .img-margin-str,
+  .img-size {
+    position: absolute;
+    left: 0;
+    top: 0;
+    z-index: -1;
+    // background-color: blue;
+  }
+  
+  // 以下元素不可见,用于计算文本的宽度
+  .almost-lottery__measureText {
+    position: absolute;
+    left: 0;
+    top: 0;
+    white-space: nowrap;
+    font-size: 12px;
+    opacity: 0;
+  }
+
+  // 以下为可见内容的样式
+  .almost-lottery__wrap {
+    position: relative;
+    // display: flex;
+    // justify-content: center;
+    // align-items: center;
+    // background-color: #FFFFFF;
+  }
+  
+  .almost-lottery__bg,
+  .almost-lottery__canvas-img,
+  .almost-lottery__action-bg {
+    position: absolute;
+    left: 0;
+    top: 0;
+  }
+  
+  .almost-lottery__canvas-img-other {
+    transition: transform cubic-bezier(.34, .12, .05, .95);
+  }
+  
+  @keyframes selfRotate {
+    0% {
+      transform: rotate(0deg);
+    }
+    50% {
+      transform: rotate(180deg);
+    }
+    100% {
+      transform: rotate(360deg);
+    }
+  }
+  
+  .almost-lottery__canvas-img-self {
+    transition: transform ease-in;
+    animation: selfRotate .6s linear infinite;
+  }
+</style>

+ 85 - 0
uni_modules/almost-lottery/package.json

@@ -0,0 +1,85 @@
+{
+  "id": "almost-lottery",
+  "displayName": "Almost-Lottery抽奖转盘",
+  "version": "1.9.7",
+  "description": "【荣获2021插件大赛三等奖】提供奇数、缓存等众多配置项,更有抽奖概率、抽奖次数、付费抽奖等功能内置于示例项目中,完美支持APP、各平台小程序、H5、PC,同时提供 uniCloud 云端版本",
+  "keywords": [
+    "转盘",
+    "抽奖",
+    "转盘抽奖",
+    "大转盘",
+    "大转盘抽奖"
+],
+  "repository": "https://github.com/ialmost/almost-components_uniapp",
+  "engines": {
+    "HBuilderX": "^3.7.11"
+  },
+"dcloudext": {
+    "sale": {
+      "regular": {
+        "price": "0.00"
+      },
+      "sourcecode": {
+        "price": "0.00"
+      }
+    },
+    "contact": {
+      "qq": ""
+    },
+    "declaration": {
+      "ads": "无",
+      "data": "无",
+      "permissions": "无"
+    },
+    "npmurl": "",
+    "type": "component-vue"
+  },
+  "uni_modules": {
+    "dependencies": [],
+    "encrypt": [],
+    "platforms": {
+      "cloud": {
+        "tcb": "y",
+        "aliyun": "y"
+      },
+      "client": {
+        "App": {
+          "app-vue": "y",
+          "app-nvue": "n"
+        },
+        "H5-mobile": {
+          "Safari": "y",
+          "Android Browser": "y",
+          "微信浏览器(Android)": "y",
+          "QQ浏览器(Android)": "y"
+        },
+        "H5-pc": {
+          "Chrome": "y",
+          "IE": "y",
+          "Edge": "y",
+          "Firefox": "y",
+          "Safari": "y"
+        },
+        "小程序": {
+          "微信": "y",
+          "阿里": "y",
+          "百度": "y",
+          "字节跳动": "y",
+          "QQ": "y",
+          "钉钉": "y",
+          "快手": "y",
+          "飞书": "y",
+        "京东": "y"
+        },
+        "快应用": {
+          "华为": "u",
+          "联盟": "u"
+        },
+        "Vue": {
+            "vue2": "y",
+            "vue3": "n"
+        }
+      }
+    }
+  }
+}

+ 174 - 0
uni_modules/almost-lottery/readme.md

@@ -0,0 +1,174 @@
+# almost-lottery
+*使用 Canvas 绘制的抽奖转盘,提供奇数、缓存等众多配置项,更有抽奖概率、抽奖次数、付费抽奖等功能内置于示例项目中*
+
+
+> <br />
+> 
+> 如果用着还行,请支持一下
+> - 前往 [GitHub](https://github.com/ialmost/almost-components_uniapp) 给个 Star
+> - 前往 [UniApp](https://ext.dcloud.net.cn/plugin?id=1030) 给个五星
+> - 使用中遇到问题时,可以添加 **QQ群 20441313**
+> 
+> <br />
+
+
+## 基于 uniCloud 开发的云端 Almost-Lottery 抽奖转盘,欢迎尝试体验
+- [Almost-Lottery抽奖转盘的云端一体页面](https://ext.dcloud.net.cn/plugin?id=5763)
+- [Almost-Lottery抽奖转盘的配置中心](https://ext.dcloud.net.cn/plugin?id=5762)
+
+
+## 高能预警
+- 本插件仅支持 `uni_modules` 模式,强烈推荐使用该模式,**非 `uni_modules` 模式不再维护**
+- 在使用本插件之前,强烈建议使用 `HBuilderX` 导入示例项目验证可用性并参照修改
+
+## 功能概要
+- [x] 可配置奖品文字 **支持横向/竖向展示**
+- [x] 可配置每个奖品区块的背景颜色
+- [x] 可配置每个奖品区块的奖品文字颜色
+- [x] 可配置奖品区块是否开启描边以及边框的颜色,默认不开启
+- [x] 可配置转盘外环和抽奖按钮图
+- [x] 可配置每个奖品区块的奖品图片,**当图片是网络地址时,小程序端需要配置白名单,H5端需要允许跨域,奖品文字为竖向时不支持展示奖品图片**
+- [x] 奖品列表支持奇数,**奇数时需尽量能被 `360` 除尽**
+- [x] 可配置内圈与外圈的间距
+- [x] 可配置轮盘旋转或指针旋转
+- [x] 可配置画板是否缓存,默认不开启
+- [x] 更多配置请查看API说明
+
+## 示例项目附加功能
+- [x] 中奖概率,**强烈推荐中奖概率应由后端控制**
+- [x] 抽奖次数
+- [x] 付费抽奖
+
+
+## 注意事项
+
+- 编译到小程序端时,请务必勾选ES6转ES5
+
+- `@reset-index="prizeIndex = -1"` 必须默认写入到 `template` 中,不可删除
+
+- 每个奖品区块的奖品图片尺寸不宜过大,图片越大,绘制的过程越慢,尽量将图片尺寸控制在 `100*100` 以内,且图片大小控制在 `40KB` 以内
+
+- 关于中奖概率的配置,请下载示例项目,参照 `pages/index/index.vue` 中的代码进行配置
+
+- 组件本身不涉及任何业务逻辑,与业务相关的代码建议都放在 `pages/index/index.vue` 中
+
+
+## 代码演示
+#### 基础用法
+```
+// template
+// @reset-index="prizeIndex = -1" 必须默认写入到 template 中,不可删除
+<almost-lottery
+  :prizeList="prizeList"
+  :prizeIndex="prizeIndex"
+  @reset-index="prizeIndex = -1"
+  @draw-before="handleDrawBefore"
+  @draw-start="handleDrawStart"
+  @draw-end="handleDrawEnd"
+  @finish="handleDrawFinish"
+  v-if="prizeList.length"
+/>
+
+// script
+import AlmostLottery from '@/uni_modules/almost-lottery/components/almost-lottery/almost-lottery.vue'
+export default {
+  components: {
+    AlmostLottery
+  },
+  data () {
+    return {
+      // 以下是奖品配置数据
+      // 奖品数据
+      prizeList: [],
+      // 中奖下标
+      prizeIndex: -1
+    }
+  },
+  methods: {
+    // 本次抽奖开始之前
+    handleDrawStart (callback) {
+      // 这里需要处理你抽奖之前的逻辑
+      // 请查看示例项目中的代码
+      // 必须调用 callback 并传递一个布尔值,布尔值为 true 时,转盘才会开始旋转
+      let flag = true
+      
+      callback(flag)
+    },
+    // 本次抽奖开始
+    handleDrawStart () {
+      // 这里需要处理你的抽奖逻辑,并得出中奖物品的 prizeIndex
+      // 请查看示例项目中的代码
+    },
+    // 本次抽奖结束
+    handleDrawEnd () {
+      // 完成抽奖后,这里处理你拿到结果后的逻辑
+      // 请查看示例项目中的代码
+    },
+    // 抽奖转盘绘制完成
+    handleDrawFinish (res) {
+      // 抽奖转盘准备就绪后,这里处理你的逻辑
+      // 请查看示例项目中的代码
+      // console.log('抽奖转盘绘制完成', res)
+    }
+  }
+}
+```
+
+## API
+#### Props
+参数 | 说明 | 类型 | 默认值
+:---|:---|:---|:---
+pixelRatio | 移动端设计稿的像素比基准值,**涉及到 `rpx` 的适配问题** | *`Number`* | `2`
+canvasId | Canvas的标识,**多画板情况下需要配置不同的标识** | *`String`* | `'almostLottery'`
+renderDelay | 转盘的渲染延时,**转盘被包裹在 uni-popup 组件内且开启了动画,请设置成最少 300** | *`Number`* | `0`
+lotterySize | 抽奖转盘的整体尺寸,单位 `rpx` | *`Number`* | `600`
+actionSize | 抽奖按钮的尺寸,单位 `rpx` | *`Number`* | `200`
+canvasMarginOutside | Canvas边缘距离转盘边缘的距离,单位`rpx` | *`Number`* | `90`
+prizeIndex | 获奖奖品在奖品列表中的序号,**每次抽奖结束后会自动重置为 `-1`** | *`Number`* | `-1`
+prizeList | 奖品列表,支持奇数(尽量能被 `360` 除尽),**为奇数时需要重设 `colors` 参数** | *`Array`* | -
+lotteryBg | 转盘外环图片 | `String` | `默认内置的本地图片`
+actionBg | 抽奖按钮图片 | `String` | `默认内置的本地图片`
+colors | 奖品区块对应的背景颜色,默认 2 个颜色相互交替,**也可以对每个区块设置不同颜色** | *`Array`* | `['#FFFFFF', '#FFBF05']`
+prizeNameDrawed | 是否绘制奖品名称 | *`Boolean`* | `true`
+stroked | 是否开启奖品区块描边 | *`Boolean`* | `false`
+strDirection | 奖品名称展示方向,可选值 `'horizontal'` => 横向 `'vertical'` => 竖向 | *`String`* | `'horizontal'`
+strokeColor | 奖品区块边框颜色 | *`String`* | `'#FFBF05'`
+rotateType | 旋转的类型,可选值 `'roulette'` => 轮盘旋转 `'pointer'` => 指针旋转 | *`String`* | `'roulette'`
+selfRotaty | 是否开启自转,开启后`duration`和`ringCount`参数不生效 | *`Boolean`* | `false`
+selfTime | 开启自转时,最少转多少毫秒 | *`Number`* | `1000`
+duration | 转盘旋转的动画时长,单位:秒 | *`Number`* | `8`
+ringCount | 旋转的圈数 | *`Number`* | `8`
+pointerPosition | 点击抽奖按钮指针的位置,可选值 `'edge'` => 指向边界 `'middle'` => 指向中间 | *`String`* | `'edge'`
+strFontColors | 奖品文字颜色,默认 2 个颜色相互交替,**也可以对每个区块的文字设置不同颜色,或仅设置一个颜色** | *`Array`* | `['#FFBF05', '#FFFFFF']`
+strFontSize | 奖品名称的字号,单位 `rpx` | *`Number`* | `24`
+strLineHeight | 奖品名称多行情况下的行高 | *`Number`* | `1.2`
+strMaxLen | 奖品名称长度限制,为`0`时不限制,**文字竖向时不生效** | *`Number`* | `12`
+strLineLen | 奖品名称在多行情况下第一行文字的长度,**文字竖向时不生效** | *`Number`* | `6`
+strMarginOutside | 奖品文字相对轮盘边缘的距离,单位 `rpx` | *`Number`* | `strFontSize 的一半`
+imgMarginStr | 奖品图片相对奖品文字的距离,单位 `rpx` | *`Number`* | `60`
+imgWidth | 奖品图片的宽度,单位 `rpx` | *`Number`* | `50`
+imgHeight | 奖品图片的高度,单位 `rpx` | *`Number`* | `50`
+imgDrawed | 是否绘制奖品图片,默认绘制 | *`Boolean`* | `true`
+imgCircled | 奖品图片是否裁切为圆形,默认不裁切 | *`Boolean`* | `false`
+successMsg | 转盘绘制成功的提示 | *`String`* | `'奖品准备就绪,快来参与抽奖吧'`
+failMsg | 转盘绘制失败的提示 | *`String`* | `'奖品仍在准备中,请稍后再来...'`
+canvasCached | 是否开启缓存,避免在数据不变的情况下重复绘制,建议在生产环境中开启 | *`Boolean`* | `false`
+
+#### Events
+事件名 | 说明 | 回调参数
+:---|:---|:---|:---
+@reset-index | 每次抽奖结束后重置获奖的序号为 `-1`,**该事件必须默认写入到 `template` 中,不可删除** | -
+@draw-before | 转盘旋转之前触发,**该事件必须默认写入到 `template` 中,不可删除** | `callback(Boolean)`
+@draw-start | 转盘旋转开始时触发 | -
+@draw-end | 转盘旋转结束时触发 | -
+@finish | Canvas转盘绘制完成时触发 | `{ ok: 绘制是否成功, data: 转盘的图片, msg: 绘制结果的提示 }`
+
+#### prizeList 数据结构
+*请按如下数据字段对你的奖品列表数据结构进行调整*
+键名 | 说明 | 类型
+:---|:---|:---
+prizeId | 奖品对应 `ID` | *`Number`*
+prizeName | 奖品名称 | *`String`*
+prizeStock | 奖品库存 | *`Number`*
+prizeWeight | 奖品权重 | *`Number`*
+prizeImage | 奖品图片地址,网络图片仅支持`http`和`https`协议 | *`String`*

BIN
uni_modules/almost-lottery/static/almost-lottery/almost-lottery__action.png


BIN
uni_modules/almost-lottery/static/almost-lottery/almost-lottery__action2x.png


BIN
uni_modules/almost-lottery/static/almost-lottery/almost-lottery__action3x.png


BIN
uni_modules/almost-lottery/static/almost-lottery/almost-lottery__bg.png


BIN
uni_modules/almost-lottery/static/almost-lottery/almost-lottery__bg2x.png


BIN
uni_modules/almost-lottery/static/almost-lottery/almost-lottery__bg3x.png


+ 298 - 0
uni_modules/almost-lottery/utils/almost-utils.js

@@ -0,0 +1,298 @@
+/**
+ * 存储 localStorage 数据
+ * @param {String} name - 缓存数据的标识
+ * @param {any} content - 缓存的数据内容
+ */
+export const setStore = (name, content) => {
+  if (!name) return
+  if (typeof content !== 'string') {
+    content = JSON.stringify(content)
+  }
+	uni.setStorageSync(name, content)
+}
+
+/**
+ * 获取 localStorage 数据
+ * @param {String} name - 缓存数据的标识
+ */
+export const getStore = (name) => {
+  if (!name) return
+  return uni.getStorageSync(name)
+}
+
+/**
+ * 清除 localStorage 数据
+ * @param {String} name - 缓存数据的标识
+ */
+export const clearStore = (name) => {
+  if (name) {
+    uni.removeStorageSync(name)
+  } else {
+    console.log('清理本地全部缓存')
+    uni.clearStorageSync()
+  }
+}
+
+/**
+ * 绘制圆形
+ * @param {String} ctx - 图片网络地址
+ * @param {String} img - 图片地址
+ * @param {String} x - x 轴偏移量
+ * @param {String} y - y 轴偏移量
+ * @param {String} w - 宽
+ * @param {String} h - 高
+*/
+export const circleImg = (ctx, img, x, y, w, h) => {
+  let r = Math.floor(w/2)
+  let cx = x + r
+  let cy = y + r
+  
+  ctx.save()
+  ctx.beginPath()
+  ctx.arc(cx, cy, r, 0, Math.PI * 2)
+  ctx.fill()
+  ctx.clip()
+  ctx.drawImage(img, x, y, w, h)
+  ctx.restore()
+}
+
+/**
+ * 计算文本的长度
+ * @param {String} text - 文本内容
+ */
+export const clacTextLen = (text) => {
+  if (!text) return { byteLen: 0, realLen: 0 }
+  text += ''
+  let clacLen = 0
+  for (let i = 0; i < text.length; i++) {
+    if ((text.charCodeAt(i) < 0) || (text.charCodeAt(i) > 255)) {
+      clacLen += 2
+    } else {
+      clacLen += 1
+    }
+  }
+  // console.log(`当前文本 ${text} 的长度为 ${clacLen / 2}`)
+  return {
+    byteLen: clacLen,
+    realLen: clacLen / 2
+  }
+}
+
+/**
+ * 下载文件,并返回临时路径
+ * @return {String}  临时路径
+ * @param {String} fileUrl - 网络地址
+*/
+export const downloadFile = (fileUrl) => {
+  return new Promise((resolve) => {
+    uni.downloadFile({
+      url: fileUrl,
+      success: (res) => {
+				resolve({
+				  ok: true,
+				  data: res.errMsg,
+				  tempFilePath: res.tempFilePath
+				})
+      },
+      fail: (err) => {
+        resolve({
+          ok: false,
+          data: err.errMsg,
+          msg: '图片下载失败'
+        })
+      }
+    })
+  })
+}
+
+/**
+ * 清理应用已缓存的文件
+*/
+export const clearCacheFile = () => {
+	// #ifndef H5
+	uni.getSavedFileList({
+		success: (res) => {
+			let fileList = res.fileList
+			if (fileList.length) {
+				for (let i = 0; i < fileList.length; i++) {
+					uni.removeSavedFile({
+						filePath: fileList[i].filePath,
+						complete: () => {
+							console.log('清除缓存已完成')
+						}
+					})
+				}
+			}
+		},
+		fail: (err) => {
+			console.log('getSavedFileList Fail')
+		}
+	})
+	// #endif
+}
+
+
+
+// 图像转换工具,可用于图像和base64的转换
+// https://ext.dcloud.net.cn/plugin?id=123
+const getLocalFilePath = (path) => {
+  if (
+    path.indexOf('_www') === 0 ||
+    path.indexOf('_doc') === 0 ||
+    path.indexOf('_documents') === 0 ||
+    path.indexOf('_downloads') === 0
+  ) return path
+
+  if (path.indexOf('/storage/emulated/0/') === 0) return path
+	
+  if (path.indexOf('/storage/sdcard0/') === 0) return path
+
+  if (path.indexOf('/var/mobile/') === 0) return path
+
+  if (path.indexOf('file://') === 0) return path
+
+  if (path.indexOf('/') === 0) {
+		// ios 无法获取本地路径
+    let localFilePath = plus.os.name === 'iOS' ? path : plus.io.convertLocalFileSystemURL(path)
+    if (localFilePath !== path) {
+      return localFilePath
+    } else {
+      path = path.substring(1)
+    }
+  }
+	
+  return '_www/' + path
+}
+
+export const pathToBase64 = (path) => {
+	return new Promise((resolve, reject) => {
+		if (typeof window === 'object' && 'document' in window) {
+			if (typeof FileReader === 'function') {
+				let xhr = new XMLHttpRequest()
+				xhr.open('GET', path, true)
+				xhr.responseType = 'blob'
+				xhr.onload = function() {
+					if (this.status === 200) {
+						let fileReader = new FileReader()
+						fileReader.onload = function(e) {
+							resolve(e.target.result)
+						}
+						fileReader.onerror = reject
+						fileReader.readAsDataURL(this.response)
+					}
+				}
+				xhr.onerror = reject
+				xhr.send()
+				return
+			}
+			let canvas = document.createElement('canvas')
+			let c2x = canvas.getContext('2d')
+			let img = new Image
+			img.onload = function() {
+				canvas.width = img.width
+				canvas.height = img.height
+				c2x.drawImage(img, 0, 0)
+				resolve(canvas.toDataURL())
+				canvas.height = canvas.width = 0
+			}
+			img.onerror = reject
+			img.src = path
+			return
+		}
+		
+		if (typeof plus === 'object') {
+			let tempPath = getLocalFilePath(path)
+			plus.io.resolveLocalFileSystemURL(tempPath, (entry) => {
+				entry.file((file) => {
+					let fileReader = new plus.io.FileReader()
+					fileReader.onload = function(data) {
+						resolve(data.target.result)
+					}
+					fileReader.onerror = function(error) {
+						console.log(error)
+						reject(error)
+					}
+					fileReader.readAsDataURL(file)
+				}, (error) => {
+					reject(error)
+				})
+			}, (error) => {
+				reject(error)
+			})
+			return
+		}
+		
+		if (typeof wx === 'object' && wx.canIUse('getFileSystemManager')) {
+			wx.getFileSystemManager().readFile({
+				filePath: path,
+				encoding: 'base64',
+				success: (res) => {
+					resolve('data:image/png;base64,' + res.data)
+				},
+				fail: (error) => {
+					reject(error)
+				}
+			})
+			return
+		}
+		reject(new Error('not support'))
+	})
+}
+
+export const base64ToPath = (base64) => {
+	return new Promise((resolve, reject) => {
+		if (typeof window === 'object' && 'document' in window) {
+			base64 = base64.split(',')
+			let type = base64[0].match(/:(.*?);/)[1]
+			let str = atob(base64[1])
+			let n = str.length
+			let array = new Uint8Array(n)
+			while (n--) {
+				array[n] = str.charCodeAt(n)
+			}
+			return resolve((window.URL || window.webkitURL).createObjectURL(new Blob([array], {
+				type: type
+			})))
+		}
+		let extName = base64.match(/data\:\S+\/(\S+);/)
+		if (extName) {
+			extName = extName[1]
+		} else {
+			reject(new Error('base64 error'))
+		}
+		let fileName = Date.now() + '.' + extName
+		if (typeof plus === 'object') {
+			let bitmap = new plus.nativeObj.Bitmap('bitmap' + Date.now())
+			bitmap.loadBase64Data(base64, () => {
+				let filePath = '_doc/uniapp_temp/' + fileName
+				bitmap.save(filePath, {}, () => {
+					bitmap.clear()
+					resolve(filePath)
+				}, (error) => {
+					bitmap.clear()
+					reject(error)
+				})
+			}, (error) => {
+				bitmap.clear()
+				reject(error)
+			})
+			return
+		}
+		if (typeof wx === 'object' && wx.canIUse('getFileSystemManager')) {
+			let filePath = wx.env.USER_DATA_PATH + '/' + fileName
+			wx.getFileSystemManager().writeFile({
+				filePath: filePath,
+				data: base64.replace(/^data:\S+\/\S+;base64,/, ''),
+				encoding: 'base64',
+				success: () => {
+					resolve(filePath)
+				},
+				fail: (error) => {
+					reject(error)
+				}
+			})
+			return
+		}
+		reject(new Error('not support'))
+	})
+}

Some files were not shown because too many files changed in this diff