Commit 1e78e8ee by 赖慧粮

feat(project): 抽奖项目nuxt化 & 优化代码逻辑,封装解耦

parents
# editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
module.exports = {
root: true,
env: {
browser: true,
node: true
},
parserOptions: {
parser: '@babel/eslint-parser',
requireConfigFile: false
},
extends: [
'@nuxtjs',
'plugin:nuxt/recommended',
'prettier'
],
plugins: [
],
// add your custom rules here
rules: {}
}
# Created by .ignore support plugin (hsz.mobi)
### Node template
# Logs
/logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# Nuxt generate
dist
nuxt-dist
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless
# IDE / Editor
.idea
# Service worker
sw.*
# macOS
.DS_Store
# Vim swap files
*.swp
{
"eslintIntegration": true,
"singleQuote": true,
"semi": false,
"jsxBracketSameLine": true,
"tabWidth": 2,
"printWidth": 120,
"endOfLine": "auto",
"trailingComma": "none",
"arrowParens": "avoid"
}
{
"cSpell.words": [
"gdyfe",
"nuxtjs"
]
}
FROM alpine AS builder
WORKDIR /home/app
RUN apk add --no-cache --update nodejs yarn
COPY package.json yarn.lock ./
RUN yarn install --registry=https://registry.yarnpkg.com
FROM registry.cn-hangzhou.aliyuncs.com/open_images/node12.13.1-pm2-zabbix
ADD ./ /var/www/web_lottery
ENV HOST 0.0.0.0
ENV TZ Asia/Shanghai
WORKDIR /var/www/web_lottery
COPY --from=builder /home/app/package.json ./package.json
COPY --from=builder /home/app/node_modules ./node_modules
RUN yarn build:prod
RUN rm -rf assets components layouts middleware pages plugins store .eslintrc.js .gitignore build.yml pDockerfile README.md pStart.sh
EXPOSE 4002
RUN chmod +x start.sh
ENTRYPOINT ["./start.sh"]
# 直播间内嵌抽奖气泡组件
## 介绍
> 采用vue远程组件(sfc)的方式,从cdn资源引入
### 使用示例
```
// index.html
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" href="https://guangdianyun.oss-cn-hangzhou.aliyuncs.com/common/components/lotteryEntry/LotteryEntry.css">
</head>
<body>
<div id="app"></div>
<script type="text/javascript" src="https://guangdianyun.oss-cn-hangzhou.aliyuncs.com/common/components/lotteryEntry/LotteryEntry.umd.min.js">
</body>
</html>
```
```
// demo.vue
<template>
<LotteryEntry></LotteryEntry>
</template>
<script>
export default {
name: 'demo',
components: {
LotteryEntry: winodw.LotteryEntry
}
}
</script>
```
### Props
| **参数** | **说明** | **类型** | **默认值** |
| ----------------- | ----------------------- | ------------------ | ------------------ |
| theme | 主题 | string | dark,可选值 light |
| liveId | 直播间id,必填 | string \|\| number | null |
| userId | 用户id | string \|\| number | null |
| includeType | 引用类型,必填 | string | live |
| retrieveApi | 查询抽奖的api方法,必填 | function | null |
| zIndex | 弹出气泡层级 | number | 1000 |
| autoClose | 打开后是否自动关闭 | boolean | true |
| autoCloseDuration | 打开后的显示时间 | Number | 15000(ms) |
### Events
| **事件名** | 说明 | 回调参数 |
| ---------- | ------------------------ | --------------------------- |
| openResult | 点击“查看中奖纪录”时触发 | value:api获取的抽奖信息对象 |
| onJoin | 点击“立即参与”时触发 | value:api获取的抽奖信息对象 |
### Methods
| 方法名 | 参数/类型 | 用途 |
| -------------------- | ------------ | ------------------------ |
| showPopover | - | 打开气泡 |
| hidePopover | - | 关闭气泡 |
| popoverUpdate | - | 更新气泡位置 |
| dmsListener | data: object | 处理dms消息 控制数据更新 |
| popHandler(已弃用) | val:boolean | 控制气泡开关 |
# web-lottery
## Build Setup
```bash
# install dependencies
$ yarn install
# serve with hot reload at localhost:3000
$ yarn dev
# build for production and launch server
$ yarn build
$ yarn start
# generate static project
$ yarn generate
```
For detailed explanation on how things work, check out the [documentation](https://nuxtjs.org).
## Special Directories
You can create the following extra directories, some of which have special behaviors. Only `pages` is required; you can delete them if you don't want to use their functionality.
### `assets`
The assets directory contains your uncompiled assets such as Stylus or Sass files, images, or fonts.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/assets).
### `components`
The components directory contains your Vue.js components. Components make up the different parts of your page and can be reused and imported into your pages, layouts and even other components.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/components).
### `layouts`
Layouts are a great help when you want to change the look and feel of your Nuxt app, whether you want to include a sidebar or have distinct layouts for mobile and desktop.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/layouts).
### `pages`
This directory contains your application views and routes. Nuxt will read all the `*.vue` files inside this directory and setup Vue Router automatically.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/get-started/routing).
### `plugins`
The plugins directory contains JavaScript plugins that you want to run before instantiating the root Vue.js Application. This is the place to add Vue plugins and to inject functions or constants. Every time you need to use `Vue.use()`, you should create a file in `plugins/` and add its path to plugins in `nuxt.config.js`.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/plugins).
### `static`
This directory contains your static files. Each file inside this directory is mapped to `/`.
Example: `/static/robots.txt` is mapped as `/robots.txt`.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/static).
### `store`
This directory contains your Vuex store files. Creating a file in this directory automatically activates Vuex.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/store).
/* eslint-disable no-confusing-arrow */
import { get, post } from '@/api/request'
import CONFIG from '@/config'
const { env, privateDomain } = CONFIG
const flag = env || CONFIG.private
// 获取地址列表
export const getAddressList = data =>
flag
? get('/mtsw/address/getAddressList', { data })
: get('/mtsw/MtswAddress/getAddressList', { data, baseURL: privateDomain })
// 删除地址
export const removeAddress = data =>
flag ? post('/mtsw/address/delAddress', data) : post('/mtsw/MtswAddress/delAddress', data, { baseURL: privateDomain })
// 编辑地址
export const setAddress = data =>
flag
? post('/mtsw/address/editAddress', data)
: post('/mtsw/MtswAddress/editAddress', data, { baseURL: privateDomain })
// 新增地址
export const addAddress = data =>
flag
? post('/mtsw/address/addAddress', data, { method: 'post' })
: post('/mtsw/MtswAddress/addAddress', data, { method: 'post', baseURL: privateDomain })
/* eslint-disable no-confusing-arrow */
/*
1# 模块分割命名请使用与模块内容较为贴近的命名,模块每个请求必须添加注释以说明用途
2# 键命名规则(建议拼接对应关键词)
查询/获取信息getxxx 新增addxxx 删除removexxx 修改/编辑/保存setxxx
3# 注意post和get传递的参数不同
4# 业务模块需要哪个接口则引入哪个
*/
import { get, post } from '@/api/request'
import CONFIG from '@/config'
const { env, privateDomain, noGateDomain, goDomain } = CONFIG
const flag = env || CONFIG.private
// 获取dms sub_key
export const getDmsInfo = data =>
flag ? get('/Program/Login/getDmsInfo', { data }) : get('/Program/Login/getDmsInfo', { data, baseURL: privateDomain })
// 获取用户信息
export const getUserInfo = data =>
flag
? get('/picText/live/Content/getUserInfo', { data })
: get('/live/live/pictext/getUserInfo', { data, baseURL: noGateDomain })
// pv 统计
export const setPvInfo = data =>
flag ? post('/Program/Live/PostInfo', data) : post('/live/postinfo', data, { baseURL: goDomain })
// 微信分享信息
export const getWxShareSecret = data =>
flag
? post('/Program/Share/getWxJsapiPackage', data)
: post('/Program/Share/getWxJsapiPackage', data, { baseURL: privateDomain })
/* eslint-disable no-confusing-arrow */
import { post, get } from '@/api/request'
import CONFIG from '@/config'
const { env, activityDomain } = CONFIG
const flag = env || CONFIG.private
// 获取抽奖(场次)列表
export const getLotterySessionList = data =>
flag
? get('/activity/Draw/getDrawList', { data })
: get('/activity/Draw/getDrawList', { data, baseURL: activityDomain })
// 获取抽奖信息
export const getLotteryInfos = data =>
flag
? get('/activity/Draw/getDrawInfo', { data })
: get('/activity/Draw/getDrawInfo', { data, baseURL: activityDomain })
// 抽奖
export const setLottery = data =>
flag ? post('/activity/Draw/drawing', data) : post('/activity/Draw/drawing', data, { baseURL: activityDomain })
// 获取抽奖记录列表
export const getLotteryRecordList = data =>
flag
? get('/activity/DrawResult/personResult', { data })
: get('/activity/DrawResult/personResult', { data, baseURL: activityDomain })
// 获取中奖名单列表
export const getWinnersList = data =>
flag
? get('/activity/Draw/getWinningRecord', { data })
: get('/activity/Draw/getWinningRecord', { data, baseURL: activityDomain })
// 获取已绑定直播间的抽奖信息
export const getLiveLottery = data =>
flag
? get('/activity/Draw/getLiveDraw', { data })
: get('/activity/Draw/getLiveDraw', { data, baseURL: activityDomain })
// 获取用户抽奖记录
export const getUserRecord = data =>
flag
? get('/activity/DrawResult/getUserRecord', { data })
: get('/activity/DrawResult/getUserRecord', { data, baseURL: activityDomain })
/* eslint-disable no-confusing-arrow */
import { post, get } from '@/api/request'
import CONFIG from '@/config'
const { env, activityDomain } = CONFIG
const flag = env || CONFIG.private
// 获取个人抽奖记录
export const getRecordList = data =>
flag
? get('/activity/DrawResult/getPersonAllRecordList', { data })
: get('/activity/DrawResult/getPersonAllRecordList', { data, baseURL: activityDomain })
// 获取个人抽奖记录详情
export const getRecordDetail = data =>
flag
? get('/activity/DrawResult/getRecordInfo', { data })
: get('/activity/DrawResult/getRecordInfo', { data, baseURL: activityDomain })
// 获取个人抽奖记录详情
export const setLotteryAddress = data =>
flag
? post('/activity/DrawResult/updateAddress', data)
: post('/activity/DrawResult/updateAddress', data, { baseURL: activityDomain })
/* eslint-disable no-confusing-arrow */
import { get } from '@/api/request'
import CONFIG from '@/config'
const { env, activityDomain } = CONFIG
const flag = env || CONFIG.private
// 获取留言抽奖中奖名单
export const getWinnersList = data =>
flag
? get('/activity/DrawResult/getWinnerRecord', { data })
: get('/activity/DrawResult/getWinnerRecord', { data, baseURL: activityDomain })
import axios from "axios";
import qs from "qs";
// import store from '@/store'
import VueCookie from "vue-cookie";
// message消息弹窗,可自行引入其他插件
import { Toast } from "vant";
// 全局配置文件
import CONFIG from "@/config";
const { env, armsPid } = CONFIG;
const arms = process.client && !CONFIG.private ? require('@/utils/arms') : null
const logger = arms && arms(armsPid, true);
// 环境变量
// 创建axios实例,不污染axios全局
const axiosService = axios.create();
// 默认content-type
axiosService.defaults.headers["Content-Type"] = "application/x-www-form-urlencoded";
// 默认baseURL
axiosService.defaults.baseURL = CONFIG.defDomain;
// 默认请求超时时间
axiosService.defaults.timeout = 3 * 1000;
// 请求拦截器
axiosService.interceptors.request.use(
config => {
const token = VueCookie.get(CONFIG.tokenKey);
const { method, headers } = config;
const data = filterNull(config.data);
// 格式化序列,目前只对post进行处理,可新增其他请求逻辑
if (
method === "post" &&
headers["Content-Type"].includes("application/x-www-form-urlencoded")
) {
config.data = qs.stringify(data);
} else if (method === "get") {
// 可以在调用的时候直接使用data而不用区分get(parms) post(data)
config.params = data;
config.data = null;
// config.data = { unused: 0 } // axios内部处理了get方法,使data无效
}
// 自定义header键:token
if (token && process.client) {
config.headers.token = token;
}
// 自定义header键:X-Ca-Stage(环境变量)
if (env) {
config.headers["X-Ca-Stage"] = env;
}
config.metadata = { startTime: new Date() };
return config;
},
error => {
return Promise.reject(error);
}
);
// 响应拦截器
axiosService.interceptors.response.use(
response => {
// 如果返回的状态码为200,说明接口请求成功,可以正常拿到数据
// 否则的话抛出错误
response.config.metadata.endTime = new Date();
response.duration = (response.config?.metadata?.endTime || 0) - (response.config?.metadata?.startTime || 0);
const { status, statusText, duration, data, config } = response;
if (status === 200) {
// console.log(data && data.errorCode === 0 && data.code === 200);
if (data && data.errorCode === 0 && data.code === 200) {
// success
} else {
// failed
logger && logger.api(config.url, true, duration, data.errorCode, data.errorMessage);
}
return Promise.resolve(response.data);
} else {
// failed
logger && logger.api(config.url, false, duration, status, statusText);
errorHandle(502);
return Promise.reject(response);
}
},
error => {
error.config.metadata.endTime = new Date();
error.duration = (error.config?.metadata?.endTime || 0) - (error.config?.metadata?.startTime || 0);
const { message, duration, config } = error;
if (message.includes("timeout")) {
// url, false, time, res.data.Flag, res.data.FlagString
logger && logger.api(config.url, false, duration, "NetworkTimeout", message);
errorHandle(408);
} else {
logger && logger.api(config.url, false, duration, "NetworkError", message);
errorHandle(504);
}
return Promise.reject(error);
}
);
/**
* 用来判断值类型
* @param {Object} obj
*/
function toType(obj) {
return {}.toString
.call(obj)
.match(/\s([a-zA-Z]+)/)[1]
.toLowerCase();
}
/**
* 对象null值过滤
* @param {Object} o 请求data对象
*/
function filterNull(o) {
for (const key in o) {
if (o[key] === null) {
delete o[key];
} else {
// eslint-disable-next-line no-lonely-if
if (toType(o[key]) === "string") {
o[key] = o[key].trim();
} else if (toType(o[key]) === "object") {
o[key] = filterNull(o[key]);
} else if (toType(o[key]) === "array") {
o[key] = filterNull(o[key]);
}
}
}
return o;
}
/**
* 根据状态码判断请求失败后的错误统一处理
* @param {Number} status 请求失败的状态码
*/
function errorHandle(status) {
let message = "";
switch (status) {
case 401:
// 401: 未登录
// 未登录则跳转登录页面,并携带当前页面的路径
// 在登录成功后返回当前页面,这一步需要在登录页操作。
message = "未授权(401),请重新登录";
// store.dispatch('info/jumpToLogin')
break;
case 403:
// 403 token过期
// 登录过期对用户进行提示
// 清除本地token和清空vuex中token对象
// 跳转登录页面
message = "拒绝访问(403),请刷新后重试";
// 清除token
// removeCookie()
// setTimeout(() => {
// store.dispatch('info/jumpToLogin')
// }, 1000)
break;
case 400:
message = "请求错误(400),请刷新后重试";
break;
case 404:
message = "请求错误(404),网络请求不存在";
break;
case 408:
message = "请求超时(408),请稍后重试";
break;
case 450:
message = "请求异常(450),请求参数错误";
break;
case 500:
message = "服务错误(500),请稍后重试!";
break;
case 502:
message = "网络错误(502),请稍后重试!";
break;
case 503:
message = "服务异常(503),正在维护,请稍等!";
break;
case 504:
message = "网络超时(504),请检查网络!";
break;
case 505:
message = "HTTP版本不受支持(505)";
break;
default:
message = `请求失败(${status}),请检查网络或联系管理员!`;
}
// console.log(message);
Toast.fail(message);
}
/**
* request
* @param {Object} config
* get、delete、head、options
* @param {String} url
* @param {Object} config
* post、put、patch
* @param {String} url
* @param {Object} data
* @param {Object} config
* 如果请求中需要使用与默认值不同的配置,直接传入config覆盖即可
*/
export const { get, post, delete: del, put, request, head, options, patch } = axiosService;
export default axiosService;
const elTransition = '0.3s height ease-in-out, 0.3s padding-top ease-in-out, 0.3s padding-bottom ease-in-out'
const Transition = {
'before-enter'(el) {
el.style.transition = elTransition
if (!el.dataset) el.dataset = {}
el.dataset.oldPaddingTop = el.style.paddingTop
el.dataset.oldPaddingBottom = el.style.paddingBottom
el.style.height = 0
el.style.paddingTop = 0
el.style.paddingBottom = 0
},
enter(el) {
el.dataset.oldOverflow = el.style.overflow
if (el.scrollHeight !== 0) {
el.style.height = el.scrollHeight + 'px'
el.style.paddingTop = el.dataset.oldPaddingTop
el.style.paddingBottom = el.dataset.oldPaddingBottom
} else {
el.style.height = ''
el.style.paddingTop = el.dataset.oldPaddingTop
el.style.paddingBottom = el.dataset.oldPaddingBottom
}
el.style.overflow = 'hidden'
},
'after-enter'(el) {
el.style.transition = ''
el.style.height = ''
el.style.overflow = el.dataset.oldOverflow
},
'before-leave'(el) {
if (!el.dataset) el.dataset = {}
el.dataset.oldPaddingTop = el.style.paddingTop
el.dataset.oldPaddingBottom = el.style.paddingBottom
el.dataset.oldOverflow = el.style.overflow
el.style.height = el.scrollHeight + 'px'
el.style.overflow = 'hidden'
},
leave(el) {
if (el.scrollHeight !== 0) {
el.style.transition = elTransition
el.style.height = 0
el.style.paddingTop = 0
el.style.paddingBottom = 0
}
},
'after-leave'(el) {
el.style.transition = ''
el.style.height = ''
el.style.overflow = el.dataset.oldOverflow
el.style.paddingTop = el.dataset.oldPaddingTop
el.style.paddingBottom = el.dataset.oldPaddingBottom
}
}
export default {
name: 'collapseTransition',
functional: true,
render(h, { children }) {
const data = {
on: Transition
}
return h('transition', data, children)
}
}
// rem 单位换算:定为 75px 只是方便运算,750px-75px、640-64px、1080px-108px,如此类推
@vw_fontsize: 75; // iPhone 6尺寸的根元素大小基准值
// @function rem($px) {
// @return ($px / $vw_fontsize) * 1rem;
// }
// 根元素大小使用 vw 单位
@vw_design: 750;
html {
font-size: (@vw_fontsize / (@vw_design / 2)) * 100vw;
// 同时,通过Media Queries 限制根元素最大最小值
@media screen and (max-width: 320px) {
font-size: 64px;
}
@media screen and (min-width: 540px) {
font-size: 108px;
}
}
// body 也增加最大最小宽度限制,避免默认100%宽度的 block 元素跟随 body 而过大过小
body {
max-width: @--body-max-width;
min-width: @--body-min-width;
margin: 0 auto !important;
}
@import './reset.less'; // 去除默认样式
@import 'normalize.css/normalize.css'; // 统一各个浏览器样式的插件
@import './adapt.less'; // 适配
html,
body {
background: #f7f7f7;
-webkit-overflow-scrolling: touch;
}
// 页面进入与离开
.page-enter-active,
.page-leave-active {
transition: all 0.25s cubic-bezier(0.55, 0, 0.1, 1);
opacity: 0;
transform: translate(30px, 0);
}
.page-enter,
.page-leave-active {
opacity: 0;
transform: translate(-30px, 0);
}
/**
* Eric Meyer's Reset CSS v2.0 (http://meyerweb.com/eric/tools/css/reset/)
* http://cssreset.com
*/
/* 去除默认样式 */
* {
box-sizing: border-box;
-webkit-overflow-scrolling: touch;
}
html,
body,
div,
span,
applet,
object,
iframe,
h1,
h2,
h3,
h4,
h5,
h6,
p,
blockquote,
pre,
a,
abbr,
acronym,
address,
big,
cite,
code,
del,
dfn,
em,
img,
ins,
kbd,
q,
s,
samp,
small,
strike,
strong,
sub,
sup,
tt,
var,
b,
u,
i,
center,
dl,
dt,
dd,
ol,
ul,
li,
fieldset,
form,
label,
legend,
table,
caption,
tbody,
tfoot,
thead,
tr,
th,
td,
article,
aside,
canvas,
details,
embed,
figure,
figcaption,
footer,
header,
menu,
nav,
output,
ruby,
section,
summary,
time,
mark,
audio,
video,
input {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font-weight: normal;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article,
aside,
details,
figcaption,
figure,
footer,
header,
menu,
nav,
section {
display: block;
}
body,
html {
line-height: 1;
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: #f7f7f7;
}
blockquote,
q {
quotes: none;
}
blockquote:before,
blockquote:after,
q:before,
q:after {
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
/* custom */
a {
color: #7e8c8d;
text-decoration: none;
-webkit-backface-visibility: hidden;
}
a:focus,
a:active {
outline: none;
}
a,
a:focus,
a:hover {
cursor: pointer;
color: inherit;
text-decoration: none;
}
li {
list-style: none;
}
html,
body {
width: 100%;
}
body {
-webkit-text-size-adjust: none;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
// 页面最大宽度限制
@--body-max-width: 540PX;
// 页面最小宽度限制
@--body-min-width: 320PX;
// 主题色
@--main-color: #3F7DF7;
# build.yml
version: 1.0
vvku:
config:
spa: false
ssr: true
folder: nuxt-dist
proxy: http://172.17.0.1:4002
server: /web\.vvku\.com/
target:
project: Web
location: lottery
command:
build: build:vvku
cbn:
config:
spa: false
ssr: true
folder: nuxt-dist
proxy: http://172.17.0.1:4002
server: /web\.cbnbn\.cn/
target:
project: Web
location: lottery
command:
build: build:cbn
huawei:
config:
spa: false
ssr: true
folder: nuxt-dist
proxy: http://172.17.0.1:34002
server: /web\.huaguangyun\.cn/
target:
project: Web
location: lottery
command:
build: build:huawei
FROM alpine AS builder
WORKDIR /home/app
RUN apk add --no-cache --update nodejs yarn
COPY package.json yarn.lock ./
RUN yarn install --registry=https://registry.yarnpkg.com
FROM registry.cn-hangzhou.aliyuncs.com/open_images/node12.13.1-pm2
ADD ./ /var/www/web_lottery
ENV HOST 0.0.0.0
ENV TZ Asia/Shanghai
WORKDIR /var/www/web_lottery
COPY --from=builder /home/app/package.json ./package.json
COPY --from=builder /home/app/node_modules ./node_modules
RUN yarn build:cbn
RUN rm -rf assets components layouts middleware pages plugins store .eslintrc.js .gitignore build.yml Dockerfile README.md start.sh
EXPOSE 4002
RUN chmod +x cbnStart.sh
ENTRYPOINT ["./cbnStart.sh"]
#!/bin/sh
yarn run pm2:cbn
while true
do
sleep 5;
done
FROM alpine AS builder
WORKDIR /home/app
RUN apk add --no-cache --update nodejs yarn
COPY package.json yarn.lock ./
RUN yarn install --registry=https://registry.yarnpkg.com
FROM registry.cn-hangzhou.aliyuncs.com/open_images/node12.13.1-pm2
ADD ./ /var/www/web_lottery
ENV HOST 0.0.0.0
ENV TZ Asia/Shanghai
WORKDIR /var/www/web_lottery
COPY --from=builder /home/app/package.json ./package.json
COPY --from=builder /home/app/node_modules ./node_modules
RUN yarn build:cm
RUN rm -rf assets components layouts middleware pages plugins store .eslintrc.js .gitignore build.yml Dockerfile README.md start.sh
EXPOSE 4002
RUN chmod +x cmStart.sh
ENTRYPOINT ["./cmStart.sh"]
#!/bin/sh
yarn run pm2:cm
while true
do
sleep 5;
done
<template>
<van-dialog v-model="visible" class="bind-phone-dialog" confirm-button-text="前往绑定" @confirm="bindMobile">
<div slot="title" class="bind-phone-dialog__title">
<span>提示</span>
<i class="bind-phone-dialog__close van-icon van-icon-cross" @click="onClose"></i>
</div>
<div class="bind-phone-dialog__content">您当前未绑定手机号,为确保联系到您,请绑定手机号</div>
</van-dialog>
</template>
<script>
import { mapActions } from 'vuex'
export default {
name: 'BindPhoneDialog',
props: {
isShow: {
type: Boolean,
default: false
}
},
data() {
return {}
},
computed: {
visible: {
get () {
return this.isShow
},
set (val) {
this.$emit('changeVisible', val)
}
}
},
methods: {
...mapActions({ jumpToBinding: 'users/jumpToBinding' }),
onClose() {
this.visible = false
this.$toast({
message: '稍后请点击“我的抽奖记录”界面绑定手机',
duration: 3500
})
},
bindMobile() {
this.$toast({
message: '正在为您转跳,请完成绑定手机操作',
duration: 1500,
onClose: () => {
this.jumpToBinding()
}
})
}
}
}
</script>
<style lang="less" scoped>
.bind-phone-dialog {
&__title {
position: relative;
}
&__close {
position: absolute;
right: 14px;
top: -12px;
color: #646566;
cursor: pointer;
font-size: 12px;
}
&__content {
padding: 24px;
padding-top: 12px;
color: #646566;
font-size: 14px;
line-height: 20px;
text-align: center;
}
}
</style>
<template>
<div class="qr-code">
<div class="qr-code__img" :style="`width:${width}px;height:${height}px`">
<img v-lazy="qrCode" alt="" />
</div>
</div>
</template>
<script>
import QRCode from 'qrcode'
export default {
name: 'QRCode',
props: {
url: {
type: String,
default: ''
},
width: {
type: Number,
default: 175
},
height: {
type: Number,
default: 175
}
},
data() {
return {
qrCode: ''
}
},
watch: {
url: {
handler (nVal) {
if (nVal) {
this.makeQrcode(nVal)
}
},
immediate: true
}
},
methods: {
makeQrcode(href) {
QRCode.toDataURL(href)
.then(url => {
this.qrCode = url
})
.catch(err => {
console.error(err)
})
}
}
}
</script>
<style lang="less" scoped>
.qr-code {
&__img {
> img {
width: 100%;
height: 100%;
display: block;
}
}
}
</style>
<template>
<vue-scroll ref="vueScroll" :ops="defaultOpt" @handle-scroll="reachBottom">
<slot></slot>
</vue-scroll>
</template>
<script>
import VueScroll from 'vuescroll'
export default {
components: {
VueScroll,
},
props: {
opt: {
type: Object,
default: () => ({}),
},
},
data() {
return {
defaultOpt: {
bar: {
background: 'rgba(0,0,0,0.2)',
keepShow: false,
size: '6px',
minSize: 0,
},
rail: {
// size: '10px',
// border: '1px solid #E6E6E6',
// gutterOfSide: "5px"
},
scrollPanel: {
scrollingX: false,
},
},
}
},
computed: {
option() {
return Object.assign(this.defaultOpt, this.opt)
},
},
methods: {
// vertical, horizontal, nativeEvent
reachBottom(vertical, horizontal, nativeEvent) {
if (vertical.process === 1) {
this.$emit('reachBottom')
}
this.$emit('onScroll', { vertical, horizontal, nativeEvent })
},
getScrollProcess() {
return this.$refs.vueScroll.getScrollProcess()
},
scrollTo(obj) {
this.$refs.vueScroll.scrollTo(obj)
},
},
}
</script>
<template>
<van-popup class="winners-popup" closeable v-model="visible" position="left">
<div class="winners-popup__container">
<div class="winners-popup__container__head">中奖名单</div>
<div class="winners-popup__container__content">
<div class="winners-popup__container__content__list">
<div class="item" v-for="(item, index) in winnersList" :key="index">
<span class="item__prize">{{ item.userNick }}获得{{ item.prizeName }}</span>
</div>
<div class="winners-popup__container__content__list__no-data" v-if="winnersList.length === 0">暂无数据</div>
</div>
</div>
</div>
</van-popup>
</template>
<script>
import { getWinnersList } from '@/api/modules/lottery'
export default {
name: 'WinnersPopup',
props: {
isShow: {
type: Boolean,
default: false
}
},
data() {
return {
winnersList: []
}
},
computed: {
visible: {
get () {
return this.isShow
},
set (val) {
this.$emit('changeVisible', val)
}
}
},
watch: {
visible(nVal) {
if (nVal) {
this.dataInit()
}
}
},
methods: {
dataInit() {
getWinnersList({
id: this.$route.query.id,
uin: this.$route.query.uin
}).then(res => {
const { code, errorCode, errorMessage, data } = res
if (code === 200 && errorCode === 0) {
this.winnersList = data
} else {
this.$toast(errorMessage)
}
})
}
}
}
</script>
<style lang="less" scoped>
@titleHeight: 46px;
.winners-popup {
height: 100%;
width: 300px;
box-shadow: 0 -1px 1px 0px #fff;
/deep/ .van-popup__close-icon {
color: #fc5147;
}
&__container {
background-color: #ff8e5e;
width: 100%;
height: 100%;
padding: 6px;
&__head {
height: @titleHeight;
line-height: @titleHeight;
text-align: center;
font-size: 16px;
font-weight: 600;
color: #fc5147;
background-color: #fcf7d5;
border-radius: 6px 6px 0 0;
}
&__content {
height: calc(100% - @titleHeight);
overflow: auto;
background-color: #fcf7d5;
border-radius: 0 0 6px 6px;
&__list {
font-size: 14px;
color: #f76b5f;
width: 90%;
margin: 0 auto;
padding: 10px 0;
line-height: 18px;
.item {
display: flex;
justify-content: center;
margin-top: 6px;
&:first-child {
margin-top: 0;
}
}
&__no-data {
color: #aaa;
text-align: center;
}
}
}
}
}
</style>
<template>
<van-popup
v-model="visible"
:close-on-click-overlay="false"
:overlay-style="{ background: '#fff' }"
class="custom-popup"
>
<div class="loading">
<AudioWave class="loading__audio"></AudioWave>
</div>
</van-popup>
</template>
<script>
import AudioWave from './modules/AudioWave'
export default {
name: 'Load',
components: {
AudioWave,
},
props: {
value: {
type: Boolean,
default: true,
},
},
computed: {
visible: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
},
},
},
}
</script>
<style lang="less" scoped>
.custom-popup {
background-color: transparent;
}
.loading {
// background: rgba(255, 255, 255, 1);
background-color: transparent;
height: 100px;
justify-content: center;
align-items: center;
display: flex;
}
</style>
<template>
<div class="audio-wave">
<span v-for="item in 5" :key="item"></span>
</div>
</template>
<script>
export default {
name: 'AudioWave'
}
</script>
<style lang="less" scoped>
// 块个数
@blockNum: 5;
// 块颜色
@colorOne: #9b59b6;
@colorTwo: #409eff;
// 块宽度
/* prettier-ignore */
@blockWidth: 9PX;
// 块高度
/* prettier-ignore */
@blockHeight: 5PX;
/* prettier-ignore */
@blockActiveHeight: 30PX;
// 块间距
/* prettier-ignore */
@blockSpacing: 2PX;
@parentWidth: (@blockWidth + @blockSpacing) * @blockNum - @blockSpacing;
// @start 开始元素
// @end 结束元素
// @index 索引标记
.audio-wave-child(@start:0, @end: 0, @index: 0 ) when (@start =< @end) {
&:nth-child(@{start}) {
animation: audio_wave 1.5s infinite ease-in-out;
animation-delay: 0.2s * @index;
}
.audio-wave-child((@start + 1), @end, @index: @index + 1);
}
@keyframes audio_wave {
0% {
height: @blockHeight;
background: @colorOne;
}
25% {
height: @blockActiveHeight;
background: @colorTwo;
}
50% {
height: @blockHeight;
background: @colorOne;
}
100% {
height: @blockHeight;
background: @colorOne;
}
}
.audio-wave {
width: @parentWidth;
height: @blockActiveHeight;
display: flex;
justify-content: space-between;
align-items: center;
> span {
display: block;
width: @blockWidth;
height: @blockHeight;
background: @colorOne;
.audio-wave-child(1, @blockNum);
}
}
</style>
<template>
<div class="rainbow">
<figure class="rainbow__wrap">
<div
v-for="item in 5"
:key="item"
class="rainbow__wrap__dot"
:class="{ 'rainbow__wrap__dot--white': item === 1 }"
></div>
</figure>
</div>
</template>
<script>
export default {
name: 'Rainbow'
}
</script>
<style lang="less" scoped>
// 父容器尺寸
@parentWidth: 100px;
@parentHeight: 100px;
// 元素尺寸
@dotWidth: 100px;
@dotHeight: 100px;
@dotColorOne: #33b5e5;
@dotColorTwo: #99cc00;
@dotColorThree: #ffbb33;
@dotColorFour: #ff4444;
// 无限旋转
@keyframes rotate {
0% {
transform: rotate(0);
}
10% {
width: @dotWidth;
height: @dotHeight;
}
66% {
width: @dotWidth / 2.6;
height: @dotHeight / 2.6;
}
100% {
transform: rotate(360deg);
width: @dotWidth;
height: @dotHeight;
}
}
// 上下 y轴上元素动画
@keyframes dotsY {
66% {
opacity: 0.1;
width: @dotWidth / 2.6;
}
77% {
opacity: 1;
width: 0;
}
}
// 左右 x轴上元素动画
@keyframes dotsX {
66% {
opacity: 0.1;
height: @dotHeight / 2.6;
}
77% {
opacity: 1;
height: 0;
}
}
// 透明变化
@keyframes flash {
33% {
opacity: 0;
border-radius: 0%;
}
55% {
opacity: 0.6;
border-radius: 100%;
}
66% {
opacity: 0;
}
}
.rainbow {
width: @parentWidth;
height: @parentHeight;
position: relative;
&__wrap {
position: absolute;
margin: auto;
top: 0;
bottom: 0;
left: 0;
right: 0;
animation: rotate 2.4s linear infinite;
width: @dotWidth;
height: @dotHeight;
&__dot {
position: absolute;
margin: auto;
width: @dotWidth / 2.6;
height: @dotHeight / 2.6;
border-radius: 100%;
transition: all 1s ease;
opacity: 0.8;
&--white {
top: 0;
bottom: 0;
left: 0;
right: 0;
background: white;
animation: flash 2.4s linear infinite;
opacity: 0;
}
&:nth-child(2) {
top: 0;
bottom: 0;
left: 0;
background: @dotColorOne;
animation: dotsY 2.4s linear infinite;
}
&:nth-child(3) {
left: 0;
right: 0;
top: 0;
background: @dotColorTwo;
animation: dotsX 2.4s linear infinite;
}
&:nth-child(4) {
top: 0;
bottom: 0;
right: 0;
background: @dotColorThree;
animation: dotsY 2.4s linear infinite;
}
&:nth-child(5) {
left: 0;
right: 0;
bottom: 0;
background: @dotColorFour;
animation: dotsX 2.4s linear infinite;
}
}
}
}
</style>
<template>
<div class="spin">
<span class="spin__dot"></span>
</div>
</template>
<script>
export default {
name: 'Spin',
props: {
msg: {
type: String,
default: ''
}
}
}
</script>
<style lang="less" scoped>
@dotWidth: 32px;
@dotHeight: 32px;
@keyframes ani-spin-bounce {
0% {
-webkit-transform: scale(0);
transform: scale(0);
}
to {
-webkit-transform: scale(1);
transform: scale(1);
opacity: 0;
}
}
.spin {
width: @dotWidth;
height: @dotHeight;
&__dot {
display: block;
border-radius: 50%;
background-color: #2d8cf0;
width: @dotWidth;
height: @dotHeight;
animation: ani-spin-bounce 1s 0s ease-in-out infinite;
}
}
</style>
<template>
<section v-if="isShowBanner" class="banner">
<a class="banner__wrap" target="_blank" :href="bannerLink">
<div class="banner__wrap__content" :style="`background-image:url('${bannerImg}')`"></div>
</a>
</section>
</template>
<script>
// 默认背景图
import CONFIG from '@/config'
import { mapGetters } from 'vuex'
export default {
name: 'Banner',
computed: {
...mapGetters({
lotteryInfo: 'lottery/lotteryInfo'
}),
isShowBanner() {
const { isShowBanner } = this.lotteryInfo
return !!isShowBanner
},
bannerImg() {
const { banner } = this.lotteryInfo
const img = banner || CONFIG.defBanner
return `${img}${CONFIG.ossImageServe}`
},
bannerLink() {
return this.lotteryInfo.url || false
}
}
}
</script>
<style lang="less" scoped>
.banner {
width: 100%;
display: flex;
&__wrap {
width: 100%;
height: 100%;
&__content {
width: 100%;
height: 100%;
border-radius: 4px;
background-size: cover;
background-position: center;
font-size: 0;
}
}
}
</style>
<template>
<section v-if="'startTime' in lotteryInfo" class="countdown-bar">
<div class="countdown-bar__status">
<template v-if="parseInt(status, 10) !== LOTTERY_STATUS.end">
<div class="countdown-bar__status__text">
{{ LOTTERY_STATUS_TXT[parseInt(status, 10)].actionLabel }}倒计时:
</div>
<van-count-down millisecond :time="time">
<template #default="timeData">
<span v-if="timeData.days" class="time-item">
{{ timeData.days | limitNum }}
<span class="time-item__day"></span>
</span>
<span class="time-item">
{{ timeData.hours | formatNum }}
<span></span>
</span>
<span class="time-item">
{{ timeData.minutes | formatNum }}
<span></span>
</span>
<span class="time-item">
{{ timeData.seconds | formatNum }}
<span></span>
</span>
</template>
</van-count-down>
</template>
<template v-if="parseInt(status, 10) === LOTTERY_STATUS.end">抽奖已结束</template>
</div>
</section>
</template>
<script>
import { mapGetters } from 'vuex'
import { LOTTERY_STATUS, LOTTERY_STATUS_TXT } from '@/utils/constant'
export default {
name: 'CountdownBar',
filters: {
formatNum(val) {
let value = val
if (val < 10) {
value = `0${val}`
}
return value
},
limitNum(val) {
let value = val
if (val > 99) {
value = `99+`
}
return value
}
},
data() {
return {
LOTTERY_STATUS,
LOTTERY_STATUS_TXT
}
},
computed: {
...mapGetters({ lotteryInfo: 'lottery/lotteryInfo' }),
status() {
return this.lotteryInfo.status
},
time() {
const { lotteryInfo, status } = this
const now = new Date().getTime()
let timeDiff
if (status === 0) {
timeDiff = lotteryInfo.startTime * 1000 - now
} else if (status === 1) {
timeDiff = lotteryInfo.endTime * 1000 - now
}
return timeDiff
}
}
}
</script>
<style lang="less" scoped>
.countdown-bar {
width: 100%;
background: url('~@/assets/images/lottery/lottery_instant_contdown_bg.png');
background-size: contain;
background-repeat: no-repeat;
background-position: center;
&__status {
transform: translateY(-4px);
font-size: 14px;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
color: #ff2b00;
font-weight: 600;
/deep/ .van-count-down {
color: #ff2b00;
.time-item {
font-weight: 600;
}
}
&__text {
font-weight: 600;
}
}
}
</style>
<template>
<section class="grid-table">
<div class="grid-table__lantern">
<span v-for="item in 26" :key="item"></span>
</div>
<div class="grid-table__prize">
<div
v-for="(item, index) in giftList"
:key="index"
class="grid-table__prize__item"
:class="{ active: activeItem === index }"
>
<div v-if="index !== 4" class="item-box">
<template v-if="parseInt(item.id, 10) !== 0">
<img :src="img" alt />
<p>{{ item.name }}</p>
</template>
<p v-else>谢谢参与</p>
</div>
<div v-else class="item-btn" @click="lottery">
<span>立即抽奖</span>
</div>
</div>
</div>
</section>
</template>
<script>
import { mapGetters } from 'vuex'
import { shuffle } from '@/utils/common'
import img from '@/assets/images/lottery/prize.png'
export default {
name: 'GridTable',
props: {
loading: {
default: false,
type: Boolean
},
winId: {
default: 0,
type: Number
}
},
data() {
return {
img,
activeItem: 5,
giftList: [],
transitionList: [0, 1, 2, 5, 8, 7, 6, 3],
timer: null,
roundOne: 3,
winItem: null
}
},
computed: {
...mapGetters({ lotteryInfo: 'lottery/lotteryInfo' }),
state() {
return this.lotteryInfo.status
},
config() {
const list =
typeof this.lotteryInfo.prizeConfigs === 'object'
? Object.values(this.lotteryInfo.prizeConfigs)
: this.lotteryInfo.prizeConfigs
return list
},
times() {
return this.lotteryInfo.userTimes
}
},
watch: {
config(nval, oval) {
if (JSON.stringify(nval) !== JSON.stringify(oval)) {
this.computList(nval)
}
},
loading(nVal) {
if (nVal) {
this.runGrid(60)
}
},
winId(val) {
let index
this.giftList.forEach((v, i) => {
if (v.id === val) {
index = i
}
})
this.winItem = index
}
},
mounted() {
this.computList(this.config)
this.dataInit()
},
methods: {
// 数据初始化
dataInit() {
this.giftList.forEach((v, i) => {
if (v.id === this.winId) {
this.winItem = i
}
})
},
// 点击触发
lottery() {
this.$emit('start')
},
// 动画运行
runGrid(speed) {
this.timer && clearInterval(this.timer)
let roundOne = 0
let sPosition = this.transitionList.indexOf(this.activeItem)
this.timer = setInterval(() => {
sPosition++
roundOne++
if (sPosition > 7) {
sPosition = 0
}
this.activeItem = this.transitionList[sPosition]
if (roundOne === this.roundOne * this.transitionList.length) {
this.runGrid(speed * 2)
}
// if (speed > 220 && this.activeItem == this.winItem && roundOne>(this.roundOne * this.transitionList.length)/2) {
if (speed > 220 && parseInt(this.activeItem, 10) === parseInt(this.winItem, 10)) {
clearInterval(this.timer)
this.$emit('end')
}
}, speed)
},
// 计算奖项的数组
computList(config) {
const levelList = []
config.forEach(item => {
levelList.push({ id: item.id, levelAlias: item.prizeAlias || '', name: item.name })
})
for (let i = 9 - levelList.length; i > 0; i--) {
levelList.push({ id: 0 })
}
this.shuffleLevelList(levelList)
},
// 把奖项从第五个格子筛选出来
shuffleLevelList(arr) {
// 洗牌
const result = shuffle(arr)
if (result[4].id !== 0) {
this.shuffleLevelList(arr)
return
} else {
result.splice(4, 1, { id: -1 })
}
this.giftList = result
}
}
}
</script>
<style lang="less">
@lantenStyle: {
background-color: #f9f4f4;
box-shadow: 0 0 3px 0 #f9f4f4;
};
@lantenActiveStyle: {
background-color: #fe3426;
box-shadow: 0 0 3px 0 #fe3426;
};
// @start 开始位置
// @end 结束位置
// @add 增量变量
.lanpositon(left, @top:0, @left:0, @start:0, @end: 0, @add: 0 ) when (@start =< @end) {
&:nth-child(@{start}) {
left: @left;
top: @add + @top;
}
.lanpositon(left, @top, @left, (@start + 1), @end, @add + @top);
}
.lanpositon(top, @top:0, @left:0, @start:0, @end: 0, @add: 0 ) when (@start =< @end) {
&:nth-child(@{start}) {
top: @top;
left: @add + @left;
}
.lanpositon(top, @top, @left, (@start + 1), @end, @add + @left);
}
@keyframes flicker {
0%,
50% {
@lantenStyle();
}
51%,
100% {
@lantenActiveStyle();
}
}
@keyframes flickerTwo {
0%,
50% {
@lantenActiveStyle();
}
51%,
100% {
@lantenStyle();
}
}
.grid-table {
width: 346px;
height: 292px;
margin: 0 auto;
background-image: url('~@/assets/images/lottery/table_bg.png');
background-size: cover;
background-repeat: no-repeat;
padding: 18px;
position: relative;
background: #ffd904;
border-radius: 14px;
&__prize {
width: 100%;
height: 100%;
display: flex;
flex-wrap: wrap;
align-items: center;
background-color: #f49805;
border-radius: 14px;
padding: 2px;
&__item {
width: 33.33333%;
height: 33.33333%;
padding: 2px;
color: #ff464c;
display: flex;
text-align: center;
&.active {
.item-box {
background-color: #fc5147;
color: #fdf3ab;
}
}
&:nth-child(-n + 3) {
margin-top: 0;
}
.item-btn {
width: 100%;
height: 100%;
overflow: hidden;
border-radius: 13px;
position: relative;
background: repeating-linear-gradient(-45deg, #fc5147, #fc5147 6px, #fd736a 6px, #fd736a 12px);
display: flex;
justify-content: center;
align-items: center;
> span {
font-size: 18px;
font-weight: 600;
color: #fdf3ab;
}
}
.item-box {
background-color: #fdf3ab;
border-radius: 13px;
display: flex;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
text-align: center;
flex-wrap: wrap;
img {
display: block;
width: 50px;
height: 50px;
}
p {
width: 100%;
padding: 0 10px 0 15px;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 16px;
font-weight: 600;
}
}
}
}
&__lantern {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
span {
position: absolute;
top: 0;
left: 0;
display: block;
width: 9px;
height: 9px;
border-radius: 50%;
background-color: #f9f4f4;
box-shadow: 0 0 3px 0 #f9f4f4;
&:first-child {
top: 12.5px;
left: 8px;
}
&:nth-child(8) {
top: 12.5px;
left: 330px;
}
&:nth-child(14) {
top: 274px;
left: 330px;
}
&:nth-child(21) {
top: 274px;
left: 8px;
}
.lanpositon(top, 6px, 46px, 2, 7, 8px);
.lanpositon(left, 44px, 332px, 9, 13, 11.4px);
.lanpositon(top, 279px, -46px, 15, 20, 330px);
.lanpositon(left, -44px, 6px, 22, 26, 275px);
&:nth-child(2n) {
@lantenStyle();
animation: flicker 1s infinite linear;
}
&:nth-child(2n + 1) {
@lantenActiveStyle();
animation: flickerTwo 1s infinite linear;
}
}
}
}
</style>
<template>
<div class="introduction">
<div class="introduction__wrap">
<div class="introduction__title">活动介绍</div>
<p class="introduction__intro">{{ lotteryInfo.intro }}</p>
<template v-if="!!lotteryInfo.isShowPrize">
<div class="introduction__title">活动奖品</div>
<div class="introduction__prize">
<ul class="introduction__list" :class="{ 'introduction__list--center': lotteryInfo.prizeConfigs.length < 3 }">
<li v-for="(item, index) in lotteryInfo.prizeConfigs" :key="index">
<img v-lazy="item.icon || img" class="introduction__item-img" alt />
<p v-if="item.prizeAlias" class="introduction__item-level">{{ item.prizeAlias }}</p>
<p class="introduction__item-name">{{ item.name }}</p>
</li>
</ul>
</div>
</template>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import img from '@/assets/images/lottery/gift.png'
export default {
name: 'Introduction',
data() {
return {
img
}
},
computed: {
...mapGetters({ lotteryInfo: 'lottery/lotteryInfo' })
}
}
</script>
<style lang="less" scoped>
.introduction {
width: 100%;
background-color: #ff8e5e;
border-radius: 6px;
padding: 8px;
&__wrap {
border-radius: 6px;
background-color: #fcf7d5;
width: 100%;
height: 100%;
padding: 6px 12px;
font-size: 12px;
color: #fc5147;
}
&__title {
font-size: 14px;
font-weight: 600;
text-align: center;
height: 20px;
line-height: 20px;
margin: 8px 0;
}
&__intro {
font-size: 14px;
padding: 5px;
line-height: 18px;
white-space: pre-line;
}
&__prize {
width: 100%;
}
&__list {
display: flex;
flex-wrap: wrap;
width: 100%;
&--center {
justify-content: center;
}
> li {
text-align: center;
width: 33.3333%;
padding: 6px;
}
}
&__item-img {
display: block;
width: 68px;
height: 68px;
margin: 0 auto;
}
&__item-level {
font-size: 14px;
height: 18px;
line-height: 18px;
margin: 5px 0 0;
}
&__item-name {
font-size: 12px;
margin: 5px 0 0;
line-height: 14px;
}
}
</style>
<template>
<section class="lottery-instant">
<Banner class="lottery-instant__banner"></Banner>
<CountdownBar class="lottery-instant__countdown"></CountdownBar>
<div class="lottery-instant__times">
您还有抽奖机会
<span>{{ lotteryInfo.userTimes }}</span
>
</div>
<GridTable
v-if="parseInt(lotteryInfo.showType, 10) === LOTTERY_STYLE.grid"
class="lottery-instant__grid"
:loading="isLottering"
:win-id="winId"
@start="lotteryCallback"
@end="runEnd"
></GridTable>
<PrizeWhell
v-if="parseInt(lotteryInfo.showType, 10) === LOTTERY_STYLE.wheel"
class="lottery-instant__whell"
:loading="isLottering"
:win-id="winId"
:info="lotteryInfo.prizeConfigs"
@start="lotteryCallback"
@end="runEnd"
></PrizeWhell>
<Records class="lottery-instant__records"></Records>
<Introduction class="lottery-instant__intro"></Introduction>
<WinPopup
:is-show="isShowWin"
:info="winInfo"
@changeVisible="changeIsShowWin"
@winCallback="winCallback"
></WinPopup>
<BindPhoneDialog :is-show="isShowBindPhone" @changeVisible="changeIsShowBindPhone"></BindPhoneDialog>
</section>
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
import { setLottery } from '@/api/modules/lottery'
import { LOTTERY_STATUS, LOTTERY_STATUS_TXT, LOTTERY_STYLE } from '@/utils/constant'
import {
Banner,
CountdownBar,
GridTable,
PrizeWhell,
Records,
Introduction,
WinPopup
} from '@/components/Lottery/Instant/index'
import BindPhoneDialog from '@/components/Common/BindPhoneDialog'
export default {
name: 'MainNow',
components: {
Banner,
CountdownBar,
GridTable,
PrizeWhell,
Records,
Introduction,
WinPopup,
BindPhoneDialog
},
props: {
info: {
type: Object,
default: () => ({})
},
state: {
type: Number,
default: 0
},
config: {
type: Array,
default: () => []
},
times: {
type: Number,
default: 0
}
},
data() {
return {
LOTTERY_STYLE,
id: this.$route.query.id || null, // 抽奖id
playId: this.$route.query.sessionId || null, // 场次id
sid: this.$route.query.sid || null, // 引用id
stype: this.$route.query.stype || null, // 引用类型
winId: null, // 所中奖的id
isShowWin: false, // 是否显示中奖提示
isShowBindPhone: false, // 是否显示绑定手机号提示
isLottering: false, // 动画run
isBtnLoading: false, // 正在请求抽奖接口
winInfo: {
id: 0, // 中奖等级
name: '谢谢参与' // 中奖等级
} // 中奖信息
}
},
computed: {
...mapGetters({
uin: 'users/uin',
isLogin: 'users/isLogin',
lotteryInfo: 'lottery/lotteryInfo',
isBindPhone: 'users/isBindPhone'
})
},
methods: {
...mapActions({ jumpToLogin: 'users/jumpToLogin', updateInfo: 'lottery/updateInfo' }),
// 改变中奖模态框显示状态
changeIsShowWin(val) {
this.isShowWin = val
},
// 改变显示提示绑定手机号显示状态
changeIsShowBindPhone(val) {
this.isShowBindPhone = val
},
// 中奖后, "我知道了"回调
winCallback() {
this.changeIsShowWin(false)
if (this.winInfo.id !== 0 && !this.isBindPhone) {
this.changeIsShowBindPhone(true)
}
},
// 抽奖回调
lotteryCallback() {
// 抽奖点击事件
if (!this.isLogin) {
this.$toast({
message: '请先登录',
duration: 1500,
onClose: () => {
this.jumpToLogin()
}
})
return
}
const status = parseInt(this.lotteryInfo.status, 10)
// 抽奖不在活动日期内
if (status !== LOTTERY_STATUS.start) {
this.$toast(`抽奖活动${LOTTERY_STATUS_TXT[status].label}`)
return
}
// 正在抽奖动画过程中或正在加载数据
if (this.isLottering || this.isBtnLoading) {
return
}
// 没有次数
if (this.lotteryInfo.userTimes <= 0) {
this.$toast('抽奖次数已用完')
return
}
this.isBtnLoading = true
const params = {
id: this.id,
playId: this.playId,
uin: this.uin,
type: this.stype || 'link',
sourceId: this.sid || this.id
}
/* test-start */
// const testArr = [1142,1143,1144,0]
// this.winId = testArr[Math.floor((Math.random()*testArr.length))]
// this.winInfo = {
// id: this.winId,
// id: 0,
// name: '谢谢参与'
// };
// this.isLottering = true;
// if(true) return
/* test-end */
setLottery(params).then(res => {
const { code, errorCode, errorMessage, data } = res
if (code === 200 && errorCode === 0) {
this.isLottering = true
/* 更新用户抽奖次数 */
let times = this.lotteryInfo.userTimes
times--
this.updateInfo({ userTimes: times })
/* 开奖信息 */
this.winId = data.id ? parseInt(data.id, 10) : 0
this.winInfo = data
} else {
this.$toast(errorMessage)
}
})
},
// 转盘停止回调
runEnd() {
this.winId = null
this.isLottering = false
this.isBtnLoading = false
this.changeIsShowWin(true)
}
}
}
</script>
<style lang="less" scoped>
@bannerHeight: 150px; // banner部分高度
@countdownHeight: 44px; // 倒计时高度
@recordsHeight: 36px; // 按钮容器高度
.lottery-instant {
position: relative;
height: 100%;
background-color: #fb3359;
padding: 15px;
overflow: auto;
-webkit-overflow-scrolling: touch;
&__banner {
height: @bannerHeight;
line-height: @bannerHeight;
}
&__countdown {
height: @countdownHeight;
line-height: @countdownHeight;
margin: 15px 0;
}
&__times {
height: 18px;
line-height: 18px;
font-size: 14px;
text-align: center;
color: #fdf3aa;
> span {
margin: 5px;
color: #fff;
font-size: 16px;
font-weight: 600;
}
}
&__grid {
margin: 18px 0;
width: 100%;
}
&__whell {
margin: 18px 0;
width: 100%;
}
&__records {
height: @recordsHeight;
margin: 0 0 18px;
}
}
</style>
<template>
<div class="prize-whell">
<div class="prize-whell__content">
<div class="prize-whell__content__wrap">
<div class="prize-whell__content__wrap__lights">
<span
v-for="index in lightNum"
:key="index"
:style="`transform: rotate(${(360 / lightNum) * index}deg)`"
></span>
</div>
<div class="prize-whell__content__wrap__insert">
<div class="prize-whell__pointer" @click.stop="beforeStart">
<span>立即 抽奖</span>
</div>
<ul class="prize-whell__prizes" :style="angleStyle">
<li
class="prize-whell__prizes__item"
v-for="(item, index) in prizes"
:key="index"
:style="`transform: rotate(${(360 / prizesLen) * index}deg) skewX(${
(360 - 90 * prizesLen) / -prizesLen
}deg);`"
>
<div
class="prize-whell__prizes__item__wrap"
:style="`transform: skewX(-${(360 - 90 * prizesLen) / -prizesLen}deg) rotate(${
360 / prizesLen - 90 - 360 / prizesLen / 2
}deg);`"
>
<div class="item__text">
<p class="item__text__line-one">{{ item.name | formatLineOne }}</p>
<p class="item__text__line-two">{{ item.name | formatLineTwo }}</p>
<p class="item__text__line-three">{{ item.name | formatLineThree }}</p>
</div>
<img class="item__img" v-if="item.id && false" :src="item.icon || giftImg" alt />
</div>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script>
import giftImg from '@/assets/images/lottery/gift.png'
import { shuffle, getRandomNum, getIsEven } from '@/utils/common'
export default {
name: 'PrizeWhell',
data() {
return {
giftImg,
lightNum: 18, // 彩灯数量
laps: 4, // 圈数
time: 2000, // ms 动画时间
timer: null, // 定时器
winItem: null, // 中奖的item
angleStyle: {}, // 旋转样式
targetAngle: 0, // 目标角度
counter: 1
}
},
props: {
info: {
type: Array,
default: () => []
},
loading: {
default: false,
type: Boolean
},
winId: {
default: 0,
type: Number
}
},
computed: {
prizes() {
const list = Array.from(this.info)
const len = list.length
if (len >= 6) {
list.push({ name: '谢谢参与', id: 0 })
if (getIsEven(len)) {
list.push({ name: '谢谢参与', id: 0 })
}
return list
}
for (let i = 6 - len; i > 0; i--) {
list.push({ name: '谢谢参与', id: 0 })
}
return shuffle(list)
},
prizesLen() {
return this.prizes.length
}
},
filters: {
formatLineOne(name) {
return name.substr(0, 6)
},
formatLineTwo(name) {
return name.substr(6, 4)
},
formatLineThree(name) {
const text = name.length > 12 ? name.substr(10, 2) + '...' : name.substr(10, 2)
return text
}
},
watch: {
loading(nVal) {
if (nVal) {
setTimeout(() => {
this.start()
}, 50)
}
},
winId(val) {
let index
this.prizes.forEach((v, i) => {
if (v.id === val) {
index = i
}
})
this.winItem = index
}
},
mounted() {
this.dataInit()
},
methods: {
// 数据初始化
dataInit() {
this.prizes.forEach((v, i) => {
if (v.id === this.winId) {
this.winItem = i
}
})
},
// 点击开始抽奖
beforeStart() {
this.$emit('start')
},
// 开始逻辑
start() {
const { winItem: index, laps, prizesLen, time } = this
/* 每个奖品的平均角度 */
const itemAngle = 360 / prizesLen
/* 指针开始角度 */
const pointAngle = 90
/* 至少要转圈数的总角度 */
const lapsAngle = 360 * laps
/* 到达中奖的那个块的最小角度 */
const ItemAngleMin = index * itemAngle
/* 指针偏移值 从最大和最小角度中取出一个随机值 */
const randomAngle = -getRandomNum(0 + 1, itemAngle - 1)
// const randomAngle = -(itemAngle/2);
/* 单圈每个块该转的角度 */
const singleAngle =
ItemAngleMin - pointAngle < 0 ? Math.abs(ItemAngleMin - pointAngle) : 360 - (ItemAngleMin - pointAngle)
/* 累积角度 */
const lapsCounter = this.counter * lapsAngle
/* 目标角度 */
this.targetAngle = lapsCounter + singleAngle + randomAngle
/* 计数器 */
this.counter += 1
/* 设置样式 */
this.angleStyle = {
transform: `rotate(${this.targetAngle}deg)`,
transition: `${time}ms all`
}
/* 定时清除样式 */
setTimeout(() => {
this.$emit('end')
}, time)
}
}
}
</script>
<style lang="less" scoped>
@wheelWidth: 100%;
@wheelHeight: 100%;
@pointWidth: 80px;
@pointHeight: 80px;
@keyframes rotate-infinite {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(-360deg);
}
}
@keyframes lights-flashing {
0% {
background: #fff;
box-shadow: 0 0 5px 0 #fff;
}
100% {
background: #ffde2b;
box-shadow: 0 0 5px 0 #ffde2b;
}
}
.prize-whell {
width: @wheelWidth;
height: 0;
padding-top: @wheelHeight;
position: relative;
/* 布局样式 */
&__content {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
border-radius: 50%;
/* 外环 */
&__wrap {
width: 100%;
height: 100%;
border-radius: 50%;
background-color: #fa3e3f;
box-shadow: 0 0 5px 2px #ed3a3a, 0 0 25px 6px #fa3e3f, inset 0px 0px 7px -2px rgba(255, 255, 255, 0.4),
inset 0px 0px 10px -1px rgba(255, 255, 255, 0.2);
padding: 18px;
position: relative;
/* 内环 */
&__insert {
box-shadow: inset 0px 0px 3px 1px #ec3327;
background-color: #ff2b00;
border-radius: 50%;
width: 100%;
height: 100%;
position: relative;
padding: 2px;
overflow: hidden;
}
/* 彩灯 */
&__lights {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 50%;
background: transparent;
animation: rotate 5s linear infinite;
> span {
position: absolute;
top: 0;
left: 0;
right: 0;
margin: 0 auto;
width: 10px;
height: 100%;
border-radius: 50%;
transform-origin: center center;
&::before {
content: '';
position: absolute;
top: 4px;
left: 0;
right: 0;
margin: 0 auto;
width: 10px;
height: 10px;
border-radius: 50%;
}
&:nth-of-type(even):before {
background: #fff;
animation: lights-flashing 1s linear infinite;
}
&:nth-of-type(odd):before {
background: #d7a945;
animation: lights-flashing 1s linear reverse infinite;
}
}
}
}
}
/* 奖品样式 */
&__prizes {
width: 100%;
height: 100%;
border-radius: 50%;
position: relative;
z-index: 1;
overflow: hidden;
&__item {
overflow: hidden;
position: absolute;
top: 0;
left: 0;
width: 50%;
height: 50%;
transform-origin: 100% 100%;
&:nth-child(even) {
background-color: #fdf3aa;
}
&:nth-child(odd) {
background-color: #f1d5b0;
}
&__wrap {
color: #ff2b00;
width: 200%;
height: 200%;
text-align: center;
// padding: 10px 50% 130%;
padding: 20px 55% 130%;
line-height: 0;
/* 文字 */
.item__text {
font-size: 14px;
height: 52px;
font-weight: 700;
line-height: 14px;
width: 100%;
padding: 0 15%;
display: flex;
flex-direction: column;
justify-content: center;
> p {
font-weight: 700;
}
}
.item__img {
display: block;
width: 40px;
height: 40px;
margin: 0px auto 0;
}
}
}
}
/* 指针样式 */
&__pointer {
position: absolute;
z-index: 2;
width: @pointWidth;
height: @pointHeight;
line-height: @pointHeight;
border-radius: 50%;
left: 50%;
top: 50%;
border: 5px solid #ff8a00;
transform: translate(-50%, -50%);
background: linear-gradient(0deg, rgba(255, 255, 255, 0) 75%, rgba(255, 255, 255, 0.9) 100%) no-repeat 100% 0,
radial-gradient(circle, #fee47b 35%, rgb(252, 193, 88) 65%) no-repeat 100% 0;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
&:after {
content: '';
position: absolute;
left: 50%;
top: -34px;
border-color: transparent;
border-bottom-color: #ff8a00;
border-style: solid;
border-width: 16px 8px;
transform: translateX(-50%);
}
> span {
font-size: 20px;
font-weight: 600;
text-align: center;
color: #fa4445;
line-height: 24px;
word-break: keep-all;
}
}
}
</style>
<template>
<van-popup v-model="visible" class="record-popup" closeable position="left">
<div class="record-popup__container">
<div class="record-popup__container__head">我的抽奖纪录</div>
<div class="record-popup__container__content">
<div class="record-popup__container__content__phone">
领奖手机号:
<span v-if="isBindPhone">{{ userInfo.phone }}</span>
<span v-else class="bind" @click="bindMobile">绑定手机号</span>
</div>
<ul v-if="Object.keys(recordList).length !== 0" class="record-popup__container__content__list">
<li v-for="(session, key, index) in recordList" :key="index">
<p v-if="session.length > 1" class="item__session">{{ NUMBER_LIST[index + 1] }}场次</p>
<div v-for="(item, itemIndex) in session" :key="itemIndex" class="item__session-info">
<span class="item__session-info__level">{{ item.prizeName }}</span>
<div class="item__session-info__time">
<span>{{ (item.drawTime * 1000) | formatDate('MM-DD HH:mm:ss') }}</span>
</div>
</div>
</li>
</ul>
<div v-if="Object.keys(recordList).length === 0" class="record-popup__container__content__no-data">
暂无记录
</div>
</div>
</div>
</van-popup>
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
import { getLotteryRecordList } from '@/api/modules/lottery'
import { NUMBER_LIST } from '@/utils/constant'
export default {
name: 'RecordPopup',
props: {
isShow: {
type: Boolean,
default: false
}
},
data() {
return {
NUMBER_LIST,
recordList: {}
}
},
computed: {
...mapGetters({
uin: 'users/uin',
userInfo: 'users/userInfo',
isBindPhone: 'users/isBindPhone'
}),
visible: {
get() {
return this.isShow
},
set(val) {
this.$emit('changeVisible', val)
}
}
},
watch: {
visible(nVal) {
if (nVal) {
this.dataInit()
}
}
},
methods: {
...mapActions({ jumpToBinding: 'users/jumpToBinding' }),
dataInit() {
getLotteryRecordList({
id: this.$route.query.id,
uin: this.uin,
playId: this.$route.query.sessionId
}).then(res => {
const { code, errorCode, errorMessage, data } = res
if (code === 200 && errorCode === 0) {
this.recordList = data
} else {
this.$toast(errorMessage)
}
})
},
bindMobile() {
this.$toast({
message: '正在为您转跳,请完成绑定手机操作',
duration: 1500,
onClose: () => {
this.jumpToBinding()
}
})
}
}
}
</script>
<style lang="less" scoped>
@titleHeight: 46px;
.record-popup {
height: 100%;
width: 300px;
box-shadow: 0 -1px 1px 0px #fff;
/deep/ .van-popup__close-icon {
color: #fc5147;
}
&__container {
background-color: #ff8e5e;
width: 100%;
height: 100%;
padding: 6px;
&__head {
height: @titleHeight;
line-height: @titleHeight;
text-align: center;
font-size: 16px;
font-weight: 600;
color: #fc5147;
background-color: #fcf7d5;
border-radius: 6px 6px 0 0;
}
&__content {
height: calc(100% - @titleHeight);
background-color: #fcf7d5;
border-radius: 0 0 6px 6px;
overflow: hidden;
overflow-y: auto;
&__phone {
color: #eda431;
font-size: 16px;
font-weight: 600;
text-align: center;
height: 36px;
line-height: 36px;
position: relative;
margin-bottom: 10px;
&:after {
position: absolute;
width: 70%;
height: 1px;
content: '';
left: 50%;
bottom: -10px;
transform: translateX(-50%);
background-color: #eda431;
}
> span {
color: #f76b5f;
&.bind {
cursor: pointer;
}
}
}
&__list {
color: #eda431;
font-size: 14px;
padding: 10px 0;
width: 85%;
margin: 0 auto;
.item {
&__session {
text-align: center;
height: 18px;
line-height: 18px;
}
&__session-info {
display: flex;
justify-content: space-between;
flex-wrap: nowrap;
line-height: 16px;
margin-top: 10px;
&:first-child {
margin-top: 0;
}
&__level {
flex: 1;
}
&__time {
width: 40%;
margin-left: 10px;
display: flex;
align-items: flex-end;
}
}
}
}
&__no-data {
color: #aaa;
text-align: center;
font-size: 14px;
height: 50px;
line-height: 50px;
}
}
}
}
</style>
<template>
<div class="records" :class="{ 'records--button-center': !isShowWinnersList }">
<van-button round color="#FDF3AB" @click="openRecord">我的抽奖记录</van-button>
<RecordPopup :is-show="isShowRecordPopup" @changeVisible="changeIsShowRecord"></RecordPopup>
<van-button v-if="isShowWinnersList" round color="#FDF3AB" @click="changeIsShowWinners(true)">中奖名单</van-button>
<WinnersPopup
v-if="isShowWinnersList"
:is-show="isShowWinnersPopup"
@changeVisible="changeIsShowWinners"
></WinnersPopup>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
import RecordPopup from '@/components/Lottery/Instant/RecordPopup'
import WinnersPopup from '@/components/Common/WinnersPopup'
export default {
name: 'Records',
components: {
RecordPopup,
WinnersPopup
},
data() {
return {
isShowRecordPopup: false, // 是否显示我的抽奖记录
isShowWinnersPopup: false // 是否显示中奖名单
}
},
computed: {
...mapGetters({ isLogin: 'users/isLogin', lotteryInfo: 'lottery/lotteryInfo' }),
isShowWinnersList() {
const { lotteryInfo } = this
return !!lotteryInfo.showResult
}
},
methods: {
...mapActions({ jumpToLogin: 'users/jumpToLogin' }),
openRecord() {
if (!this.isLogin) {
this.$toast({
message: '请先登录,正在为您转跳',
duration: 1500,
onClose: () => {
this.jumpToLogin()
}
})
} else {
this.changeIsShowRecord(true)
}
},
changeIsShowRecord(val) {
this.isShowRecordPopup = val
},
changeIsShowWinners(val) {
this.isShowWinnersPopup = val
}
}
}
</script>
<style lang="less" scoped>
.records {
display: flex;
justify-content: space-between;
width: 100%;
&--button-center {
justify-content: center;
}
/deep/ .van-button {
height: 100%;
.van-button__content {
color: #fc5147;
}
}
}
</style>
<template>
<van-popup class="win-popup" v-model="visible" closeable close-icon="close" :style="{ background: 'transparent' }">
<div class="win-popup__container">
<div class="win-popup__container__wrap">
<div class="win-popup__container__wrap__title">
<template v-if="info.id !== 0">恭喜获得</template>
</div>
<div class="win-popup__container__wrap__content">
<div class="win-popup__container__wrap__content__box">
<!-- :class="{'win-popup__container__wrap__content__box--no': info.id === 0}" -->
<div class="win-popup__container__wrap__content__box__title">
<template v-if="info.id !== 0">{{ info.name }}</template>
<template v-if="info.id === 0">谢谢参与</template>
</div>
<div
class="win-popup__container__wrap__content__box__win-msg"
:class="{ 'win-popup__container__wrap__content__box__win-msg--no': info.id === 0 }"
>
<template v-if="info.id !== 0">
已成功获取{{ info.name }}
<p>根据活动说明进行领取</p>
</template>
<template v-if="info.id === 0">谢谢参与,再接再厉</template>
</div>
</div>
<van-button class="win-popup__container__wrap__content__sure" color="#F5E952" round @click="know"
>我知道了</van-button
>
</div>
</div>
</div>
</van-popup>
</template>
<script>
export default {
name: 'WinPopup',
data() {
return {}
},
props: {
isShow: {
type: Boolean,
default: false
},
info: {
type: Object,
default: () => ({})
}
},
computed: {
visible: {
get: function () {
return this.isShow
},
set: function (val) {
this.$emit('changeVisible', val)
}
}
},
components: {},
methods: {
know() {
this.$emit('winCallback')
}
}
}
</script>
<style lang="less" scoped>
@containerWidth: 326px;
@wrapWidth: 275px;
@wrapHeight: 292px;
@bulinbulinWidth: @containerWidth;
@bulinbulinHeight: 136px;
.win-popup {
overflow: visible;
/deep/ .van-popup__close-icon {
right: 52px;
top: -8px;
font-size: 38px;
color: #fff;
}
&__container {
width: 100%;
height: 100%;
position: relative;
width: @containerWidth;
&::after {
content: '';
position: absolute;
width: @bulinbulinWidth;
height: @bulinbulinHeight;
top: 40px;
left: 0;
background-image: url('~@/assets/images/lottery/bulinbulin.png');
background-size: cover;
background-repeat: no-repeat;
}
&__wrap {
margin: 0 auto;
width: 275px;
height: 292px;
padding: 10px;
border-radius: 10px;
overflow: hidden;
background-image: url('~@/assets/images/lottery/winning_bg.png');
background-size: cover;
background-repeat: no-repeat;
&__title {
padding: 0 42px;
width: 100%;
text-align: center;
color: #fc5147;
font-weight: 700;
font-size: 16px;
height: 18px;
text-indent: 12px;
margin: 45px 0 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
&__content {
color: #fc575a;
font-size: 16px;
&__box {
margin-top: 5px;
text-align: center;
color: #fc575a;
font-size: 28px;
text-indent: 12px;
font-weight: 700;
&__title {
height: 52px;
line-height: 28px;
width: 60%;
margin: 0 auto;
// 两行隐藏
word-break: break-all;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
overflow: hidden;
/*! autoprefixer: off */
-webkit-box-orient: vertical;
}
&__win-msg {
padding: 0 20px;
margin-top: 48px;
font-size: 20px;
color: #fff;
text-indent: 10px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
&--no {
margin-top: 70px;
}
> p {
color: #fe9746;
font-size: 12px;
height: 26px;
line-height: 26px;
}
}
}
&__sure {
width: 45%;
height: 38px;
line-height: 38px;
border: 0;
outline: none;
position: absolute;
bottom: 28px;
left: 50%;
transform: translateX(-50%);
/deep/ .van-button__text {
font-size: 16px;
color: #fc5147;
font-weight: 700;
}
}
}
}
}
}
</style>
// 轮播占位
export { default as Banner } from '@/components/Lottery/Instant/Banner'
// 倒计时提醒
export { default as CountdownBar } from '@/components/Lottery/Instant/CountdownBar'
// 九宫格抽奖
export { default as GridTable } from '@/components/Lottery/Instant/GridTable'
// 大转盘抽奖
export { default as PrizeWhell } from '@/components/Lottery/Instant/PrizeWhell'
// 我的抽奖记录, 中奖名单
export { default as Records } from '@/components/Lottery/Instant/Records'
// 活动介绍
export { default as Introduction } from '@/components/Lottery/Instant/Introduction'
// 中奖弹框提醒
export { default as WinPopup } from '@/components/Lottery/Instant/WinPopup'
<!-- 公共模态框 -->
<template>
<transition name="modal-fade">
<div v-if="isOpen" class="detail-modal">
<div class="modal-body">
<div class="modal-decorations"></div>
<div class="detail-content">
<div class="modal-title">
<slot name="title"></slot>
</div>
<div class="scroll-container">
<vue-scroll :ops="scrollOpt">
<slot name="content"></slot>
</vue-scroll>
</div>
</div>
<div v-if="close" class="modal-close" @click="isOpen = false">
<i class="iconfont icon-X"></i>
</div>
</div>
</div>
</transition>
</template>
<script>
// 展开收起组件
export default {
name: 'PubModal',
props: {
close: {
default: true,
type: Boolean
},
isShow: {
default: false,
type: Boolean
}
},
data() {
return {
scrollOpt: {
bar: {
background: '#eda431',
keepShow: true,
size: '6px',
minSize: 0
},
scrollPanel: {
scrollingX: false
}
}
}
},
computed: {
isOpen: {
get() {
// if (val) {
// this.autoClose();
// }
return this.isShow
},
set(val) {
this.$emit('changeVisible', val)
}
}
},
methods: {
// autoClose() {
// time && clearTimeout(time);
// const time = setTimeout(() => {
// this.isShow = false;
// }, 1200);
// }
}
}
</script>
<style lang="less" scoped>
.modal-fade-enter-active,
.modal-fade-leave-active {
transition: 0.3s all ease-in-out;
}
.modal-fade-enter,
.modal-fade-leave-to {
opacity: 0;
}
.detail-modal {
top: 0;
left: 0;
position: absolute;
z-index: 9;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
.modal-body {
width: 275px;
height: 292px;
background-image: url('~@/assets/images/lottery/detail_bg.png');
background-size: cover;
background-repeat: no-repeat;
border-radius: 10px;
position: relative;
padding: 10px;
.detail-content {
width: 100%;
height: 100%;
position: relative;
display: flex;
flex-direction: column;
.modal-title {
padding: 0 42px;
// height: 48px;
// line-height: 48px;
width: 100%;
text-align: center;
color: #eda431;
font-weight: 700;
font-size: 16px;
height: 18px;
// line-height: 16px;
margin: 16px 0 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.scroll-container {
flex: 1;
padding: 0 16px 16px;
overflow: hidden;
}
}
.modal-decorations {
position: absolute;
width: 326px;
height: 136px;
top: 41px;
left: -24px;
background-image: url('~@/assets/images/lottery/bulinbulin.png');
background-size: cover;
background-repeat: no-repeat;
}
.modal-close {
position: absolute;
cursor: pointer;
right: 0px;
width: 38px;
height: 38px;
// line-height: 35px;
text-align: center;
color: #fff;
border: 1px solid #fff;
opacity: 0.5;
border-radius: 50%;
top: -48px;
display: flex;
justify-content: center;
align-items: center;
.icon-X {
font-size: 14px;
}
}
}
}
</style>
<template>
<div class="back-box">
<van-button class="back-btn" @click="back">返回直播间</van-button>
</div>
</template>
<script>
export default {
props: {
url: {
type: String,
default: ''
}
},
methods: {
back() {
window.location.href = this.url
}
}
}
</script>
<style lang="less" scoped>
.back-btn {
border: 0;
display: block;
outline: none;
background-color: #e95c53;
color: #fff;
margin: 0 auto;
width: 180px;
height: 44px;
font-size: 14px;
line-height: 44px;
text-align: center;
}
</style>
<template>
<section class="join-button">
<div class="btn-box">
<template v-if="parseInt(lotteryInfo.status, 10) === LOTTERY_STATUS.teaser">
<button v-if="lotteryInfo.condition === 1" class="join-btn pre">未开始</button>
<button v-if="lotteryInfo.condition === 2" class="join-btn pre countdown">
<template v-if="hours > 0">{{ hours | filterNum }}:</template>
{{ minutes | filterNum }}:{{ seconds | filterNum }}后开始
</button>
</template>
<template v-if="parseInt(lotteryInfo.status, 10) === LOTTERY_STATUS.start">
<template v-if="lotteryInfo.userTimes === 0 && lotteryInfo.isDraw">
<button class="join-btn pre">待开奖</button>
</template>
<template v-else>
<div class="btn-mask"></div>
<button class="join-btn" @click="lottery">点击参加</button>
</template>
</template>
<template v-if="parseInt(lotteryInfo.status, 10) === LOTTERY_STATUS.end">
<template v-if="isLogin && lotteryInfo.isDraw && lotteryInfo.isWin">
<div class="win-result-box">
<p class="win-result">恭喜您中奖!</p>
<a v-if="isBindPhone" class="blue-text" @click="jumpToBinding">
点击绑定手机号
<i class="iconfont icon-zhankai"></i>
</a>
<span v-if="lotteryInfo.showResult" class="blue-text win-list" @click="$emit('openWiner', true)">
查看中奖名单
<i class="iconfont icon-zhankai"></i>
</span>
</div>
</template>
<template v-if="isLogin && lotteryInfo.isDraw && !lotteryInfo.isWin">
<div class="win-result-box">
<p class="win-result">很遗憾,您没有中奖</p>
<span v-if="lotteryInfo.showResult" class="blue-text win-list" @click="$emit('openWiner', true)">
查看中奖名单
<i class="iconfont icon-zhankai"></i>
</span>
</div>
</template>
<template v-if="!isLogin || !lotteryInfo.isDraw">
<button class="join-btn end">已结束</button>
</template>
</template>
</div>
<div
v-if="parseInt(lotteryInfo.status, 10) !== LOTTERY_STATUS.end ? true : !isLogin || !lotteryInfo.isDraw"
class="join-num"
>
已有 {{ lotteryInfo.activeNum }} 人参与
</div>
</section>
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
import { LOTTERY_STATUS } from '@/utils/constant'
// 倒计时逻辑处理
import CountDown from '@/mixins/CountDown'
export default {
filters: {
filterNum(val) {
return val > 0 ? (val < 10 ? `0${val}` : val) : '00'
}
},
mixins: [CountDown],
data() {
return {
LOTTERY_STATUS,
timer: null
}
},
computed: {
...mapGetters({
isLogin: 'users/isLogin',
lotteryInfo: 'lottery/lotteryInfo',
isBindPhone: 'users/isBindPhone'
})
},
watch: {
lotteryInfo: {
handler() {
this.dateInit()
},
deep: true
}
},
mounted() {
this.dateInit()
},
methods: {
...mapActions({ jumpToBinding: 'users/jumpToBinding' }),
dateInit() {
// 倒计时开始
if (this.lotteryInfo.status === 0) {
this.lotteryInfo.startTime && this.reloadTime(this.lotteryInfo.startTime * 1000)
}
},
reloadTime(time) {
this.timer && clearTimeout(this.timer)
this.timer = setTimeout(() => {
this.timeFn(time)
this.reloadTime(time)
}, 1000)
},
lottery() {
this.$emit('lottery')
}
}
}
</script>
<style lang="less">
@keyframes wave {
0% {
opacity: 0.5;
}
100% {
opacity: 1;
transform: scale(1.1);
}
}
@keyframes wavetwo {
0% {
opacity: 1;
}
100% {
opacity: 0.5;
}
}
.join-button {
.btn-box {
width: 158px;
height: 100%;
margin: 0 auto;
display: flex;
justify-content: center;
align-items: center;
position: relative;
flex-wrap: wrap;
.btn-mask {
border-radius: 50%;
opacity: 1;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
margin: auto;
width: 142px;
height: 142px;
&::before {
content: '';
border-radius: 50%;
opacity: 0.5;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #fbaca6;
animation: wave 1s ease-out infinite;
}
&:after {
content: '';
border-radius: 50%;
opacity: 0.5;
position: absolute;
top: 0;
left: 0;
width: 142px;
height: 142px;
background-color: #fbaca6;
transform: scale(1.1);
animation: wavetwo 1s ease-out infinite;
// animation-delay: 0.5s;
}
}
.join-btn {
border: 0;
padding: 0;
color: #fff;
font-size: 18px;
text-align: center;
width: 142px;
height: 142px;
line-height: 142px;
border-radius: 50%;
background-color: #e85c52;
outline: none;
position: relative;
z-index: 1;
&.pre {
background-color: #e77e7b;
}
&.end {
background-color: #999;
}
&.countdown {
font-size: 16px;
}
}
.win-result-box {
min-height: 54px;
text-align: center;
.win-result {
font-size: 18px;
color: #e95c53;
text-align: center;
white-space: nowrap;
margin-bottom: 10px;
line-height: 24px;
}
.blue-text {
color: #409eff;
font-size: 14px;
line-height: 20px;
display: block;
.icon-zhankai {
font-size: 12px;
}
}
}
}
.join-num {
margin-top: 12px;
font-size: 14px;
color: #000;
height: 20px;
line-height: 20px;
text-align: center;
}
// .test {
// opacity: 1;
// display: block;
// background: none;
// position: relative;
// border-radius: 50%;
// width: 24px;
// height: 24px;
// margin: 0 0 0 80px !important;
// background: rgba(255, 255, 255, 0.1);
// transition: 0.6s;
// &::before {
// position: absolute;
// width: 18px;
// height: 18px;
// border-radius: 50%;
// content: "";
// border: 1px solid #fff;
// left: 50%;
// top: 50%;
// transition: 0.4s;
// transform: translate(-50%, -50%);
// }
// &:after {
// position: absolute;
// width: 8px;
// height: 8px;
// border-radius: 50%;
// content: "";
// background: #fff;
// left: 50%;
// top: 50%;
// transition: 0.5s;
// transform: translate(-50%, -50%);
// }
// &:hover {
// &:before {
// background: #fff;
// width: 10px;
// height: 10px;
// border: 0px solid #00c0ff;
// }
// &:after {
// width: 24px;
// height: 24px;
// background: none;
// }
// }
// }
}
</style>
<template>
<section class="main-timing">
<TimingBanner class="timing-banner" :info="config[0]"></TimingBanner>
<div class="prize-info">
<label>奖品:</label>
<p>
<span class="text-name">{{ config[0].name }}</span>
<span class="text-num">x {{ config[0].sum }}</span>
</p>
</div>
<TimingStatus class="timing-status-box"></TimingStatus>
<JoinButton class="join-button-box" @lottery="lotteryCallback" @openWiner="changeIsShowWinner"></JoinButton>
<BackButton v-if="backUrl" class="back-box" :url="backUrl"></BackButton>
<WinnersPopup :is-show="isShowWinner" @changeVisible="changeIsShowWinner"></WinnersPopup>
</section>
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
import { setLottery } from '@/api/modules/lottery'
// banner
import TimingBanner from '@/components/Lottery/Timing/TimingBanner'
// 状态信息
import TimingStatus from '@/components/Lottery/Timing/TimingStatus'
// 参加按钮
import JoinButton from '@/components/Lottery/Timing/JoinButton'
// 返回直播间按钮
import BackButton from '@/components/Lottery/Timing/BackButton'
// 中奖名单
import WinnersPopup from '@/components/Common/WinnersPopup'
export default {
components: {
TimingBanner,
TimingStatus,
JoinButton,
BackButton,
WinnersPopup
},
data() {
return {
isBtnLoading: false,
isShowWinner: false,
id: this.$route.query.id,
playId: this.$route.query.sessionId
}
},
computed: {
...mapGetters({
uin: 'users/uin',
isLogin: 'users/isLogin',
lotteryInfo: 'lottery/lotteryInfo',
isBindPhone: 'users/isBindPhone'
}),
backUrl() {
return this.$route.query.backUrl || ''
},
config() {
const list =
typeof this.lotteryInfo.prizeConfigs === 'object'
? Object.values(this.lotteryInfo.prizeConfigs)
: this.lotteryInfo.prizeConfigs
return list
}
},
methods: {
...mapActions({ jumpToLogin: 'users/jumpToLogin', updateInfo: 'lottery/updateInfo' }),
changeIsShowWinner(val) {
this.isShowWinner = val
},
// 抽奖回调
lotteryCallback() {
// 抽奖点击事件
if (!this.isLogin) {
this.$toast({
message: '请先登录',
duration: 1500,
onClose: () => {
this.jumpToLogin()
}
})
} else if (this.lotteryInfo.status !== 1) {
// 抽奖不在活动日期内
} else if (this.isBtnLoading) {
// 正在抽奖动画过程中或正在加载数据
} else if (this.lotteryInfo.userTimes <= 0) {
this.$toast('抽奖次数已用完')
} else {
this.isBtnLoading = true
const params = {
id: this.id,
uin: this.uin,
playId: this.playId,
type: this.stype || 'link',
sourceId: this.sid || this.id
}
setLottery(params).then(res => {
const { code, errorCode, errorMessage } = res
if (code === 200 && errorCode === 0) {
let times = this.lotteryInfo.userTimes
times--
let num = this.lotteryInfo.activeNum
num++
this.updateInfo({
userTimes: times,
isDraw: 1,
activeNum: num
})
this.$toast.success('参与成功')
this.isBtnLoading = false
} else {
this.$toast.fail(errorMessage)
}
})
}
}
}
}
</script>
<style lang="less">
.main-timing {
height: 100%;
position: relative;
.timing-banner {
padding-top: 9 / 16 * 100%;
position: relative;
}
.prize-info {
margin-top: 10px;
padding-left: 16px;
min-height: 20px;
line-height: 20px;
font-size: 16px;
display: flex;
align-items: center;
justify-content: left;
label {
min-width: 50px;
}
p {
flex: 1;
span {
color: #000;
&.text-num {
margin-left: 4px;
color: #999;
}
}
}
}
.timing-status-box {
padding-left: 16px;
margin-top: 10px;
height: 20px;
line-height: 20px;
font-size: 14px;
}
.join-button-box {
margin-top: 50px;
min-height: 158px;
}
.back-box {
position: absolute;
bottom: 16px;
left: 0;
width: 100%;
border-radius: 22px;
}
}
</style>
<template>
<section>
<div class="banner-box">
<a target="_blank" :href="bannerLink">
<img :src="bannerImg" alt />
</a>
</div>
</section>
</template>
<script>
import CONFIG from '@/config'
export default {
props: {
info: {
type: String,
default: ''
}
},
computed: {
bannerImg() {
const { banner } = this.info
const img = banner || CONFIG.defBanner
return `${img}${CONFIG.ossImageServe}`
},
bannerLink() {
const { url } = this.info
return url || false
}
}
}
</script>
<style lang="less">
.banner-box {
padding: 10px;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
img {
border-radius: 6px;
display: block;
width: 100%;
height: 100%;
}
}
</style>
<template>
<section class="timing-status">
<template v-if="type === 1">
<div v-if="state !== 2" class="countdown-box">
<label>倒计时:</label>
<p>
<span class="text-time">
<template v-if="hours > 0">{{ hours | filterNum }} : </template>{{ minutes | filterNum }} :
{{ seconds | filterNum }}
<template v-if="state === 0">后开始</template>
<template v-else-if="state === 1">后结束</template>
</span>
</p>
</div>
<span v-else class="end-lottery">抽奖已结束</span>
</template>
<template v-if="type === 2">
<div class="joinnum-box">
<p>参与人数满 {{ lotteryInfo.joinNum }} 人自动开奖(当前参与 {{ lotteryInfo.activeNum }} 人)</p>
</div>
</template>
</section>
</template>
<script>
// 倒计时逻辑处理
import CountDown from '@/mixins/CountDown'
import { mapGetters } from 'vuex'
export default {
filters: {
filterNum(val) {
return val > 0 ? (val < 10 ? `0${val}` : val) : '00'
}
},
mixins: [CountDown],
data() {
return {
stateText: {
0: '抽奖还未开始',
1: '抽奖进行中...',
2: '抽奖已结束'
},
timer: null
}
},
computed: {
...mapGetters({ lotteryInfo: 'lottery/lotteryInfo' }),
state() {
return this.lotteryInfo.status
},
type() {
return this.lotteryInfo.condition
}
},
watch: {
state(nval, oval) {
if (nval !== oval) {
this.dateInit()
}
},
lotteryInfo: {
handler() {
this.dateInit()
},
deep: true
}
},
mounted() {
this.dateInit()
},
methods: {
dateInit() {
// 倒计时开始
if (this.state === 0) {
this.lotteryInfo.startTime && this.reloadTime(this.lotteryInfo.startTime * 1000)
} else if (this.state === 1) {
this.lotteryInfo.endTime && this.reloadTime(this.lotteryInfo.endTime * 1000)
}
},
reloadTime(time) {
this.timer && clearTimeout(this.timer)
this.timer = setTimeout(() => {
this.timeFn(time)
this.reloadTime(time)
}, 1000)
}
}
}
</script>
<style lang="less">
.timing-status {
.countdown-box {
display: flex;
align-items: center;
justify-content: left;
label {
color: #999999;
min-width: 58px;
}
p {
flex: 1;
span {
&.text-time {
font-size: 16px;
font-weight: 700;
color: #e85c52;
}
}
}
}
.end-lottery {
font-size: 14px;
color: #999;
}
.joinnum-box {
font-size: 14px;
p {
color: #999999;
}
}
}
</style>
<!-- 绑定手机号 -->
<template>
<PubModal :is-show="isOpen" @changeVisible="changeVisible">
<template slot="title">中奖名单</template>
<div slot="content" class="winner-info">
<ul>
<li v-for="(item, index) in winnersList" :key="index">
<span>{{ item | omitPhone }}</span>
<span class="prize-name">{{ item.prizeName }} x 1</span>
</li>
</ul>
</div>
</PubModal>
</template>
<script>
import PubModal from '@/components/Lottery/PubModal'
import { mapGetters } from 'vuex'
import { getWinnersList } from '@/api/modules/lottery'
export default {
name: 'BindModal',
components: {
PubModal
},
filters: {
omitPhone(item) {
if (item.phone) {
return item.phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
} else {
return item.userNick
}
}
},
props: {
isShow: {
default: true,
type: Boolean
}
},
data() {
return {
winnersList: []
}
},
computed: {
...mapGetters({ lotteryInfo: 'lottery/lotteryInfo' }),
isOpen: {
get () {
return this.isShow
},
set (val) {
this.$emit('changeVisible', val)
}
}
},
watch: {
isOpen(nVal) {
if (nVal) {
this.dataInit()
}
}
},
methods: {
changeVisible(val) {
this.isOpen = val
},
dataInit() {
getWinnersList({
id: this.$route.query.id,
uin: this.$route.query.uin
}).then(res => {
const { code, errorCode, errorMessage, data } = res
if (code === 200 && errorCode === 0) {
this.winnersList = data
} else {
this.$toast(errorMessage)
}
})
}
}
}
</script>
<style lang="less" scoped>
.winner-info {
color: #eda431;
overflow: hidden;
ul {
padding: 10px 5px;
li {
display: flex;
justify-content: space-between;
span {
font-size: 12px;
&.prize-name {
padding-left: 6px;
}
}
}
}
}
</style>
<template>
<van-popup
v-model="visible"
class="detail-popup"
closeable
:overlay="false"
position="right"
get-container="body"
:style="{ height: '100%', width: '100%' }"
>
<div class="detail-popup__content">
<ul class="detail-popup__info">
<li>
<label>活动名称:</label>
<span>{{ detailInfo.title }}</span>
</li>
<li>
<label>中奖场次:</label>
<span v-if="detailInfo.drawPlay">{{ `第 ${detailInfo.drawPlay} 场` }}</span>
</li>
<li>
<label>参与时间:</label>
<span v-if="detailInfo.drawTime">{{ (detailInfo.drawTime * 1000) | formatDate('YYYY-MM-DD HH:mm') }}</span>
</li>
<li>
<label>获得奖品:</label>
<div>
<p>{{ detailInfo.prizeName }}</p>
<img v-lazy="detailInfo.prizeIcon" class="detail-popup__info-img" alt="" />
</div>
</li>
<li>
<label>领奖方式:</label>
<span>{{ LOTTERY_EXCHANGE_TYPE_TXT[detailInfo.exchangeType] }}</span>
</li>
<li v-if="detailInfo.exchangeType === LOTTERY_EXCHANGE_TYPE.mailing">
<label>收货地址:</label>
<span v-if="detailInfo.address">{{ detailInfo.address }}</span>
<span v-if="!detailInfo.address" class="detail-popup__edit-btn" @click="isShowAddressPopup = true"
>点击填写收货地址</span
>
</li>
<li v-if="detailInfo.exchangeType === LOTTERY_EXCHANGE_TYPE.mailing && detailInfo.contactPhone">
<label>联系方式:</label>
<span>{{ detailInfo.contactPhone }}</span>
</li>
</ul>
<div v-if="detailInfo.exchangeType === LOTTERY_EXCHANGE_TYPE.offline" class="detail-popup__code-info">
<QRCode :url="detailInfo.code" :width="70" :height="70"></QRCode>
<p class="detail-popup__code-text"><label>兑换码:</label>{{ detailInfo.code }}</p>
<p class="detail-popup__notice">*兑换信息请勿泄露给他人,以防冒充代领</p>
</div>
</div>
<AddressListPopup
v-model="isShowAddressPopup"
left-text="保存"
:uin="uin"
:create-api="addAddress"
:update-api="setAddress"
:retrieve-api="getAddressList"
:delete-api="removeAddress"
@onBackHandler="onSaveAddress"
></AddressListPopup>
</van-popup>
</template>
<script>
import { getRecordDetail, setLotteryAddress } from '@/api/modules/records'
import { LOTTERY_EXCHANGE_TYPE, LOTTERY_EXCHANGE_TYPE_TXT } from '@/utils/constant'
import { AddressListPopup } from '@gdyfe/gdy-component-lib'
import '@gdyfe/gdy-component-lib/lib/index.css'
import { getAddressList, removeAddress, setAddress, addAddress } from '@/api/modules/address'
export default {
name: 'DetailPopup',
components: {
QRCode: () => import('@/components/Common/QRCode'),
AddressListPopup
},
props: {
value: {
type: Boolean,
default: false
},
id: {
type: Number,
default: null
}
},
data() {
return {
uin: this.$route?.query?.uin || '',
getAddressList,
removeAddress,
setAddress,
addAddress,
LOTTERY_EXCHANGE_TYPE,
LOTTERY_EXCHANGE_TYPE_TXT,
detailInfo: this.detailInfoTranslator(),
isShowAddressPopup: false
}
},
computed: {
visible: {
get () {
return this.value
},
set (val) {
this.$emit('input', val)
}
}
},
watch: {
visible(nVal) {
if (nVal) {
this.loadData()
}
}
},
methods: {
loadData() {
getRecordDetail({ uin: this.$route?.query?.uin || '', id: this.id }).then(res => {
const { code, errorCode, errorMessage, data } = res
if (code === 200 && errorCode === 0) {
this.detailInfo = this.detailInfoTranslator(data)
} else {
this.$toast(errorMessage)
}
})
},
onSaveAddress(val) {
if (!val) {
this.$toast('未选择地址')
return
}
const params = {
id: this.id,
uin: this.$route?.query?.uin || '',
address: `${val?.province || ''}-${val?.city || ''}-${val?.area || ''}-${val?.detail}`,
contactPhone: val?.mobile || '',
name: val?.name || ''
}
setLotteryAddress(params).then(res => {
const { code, errorCode, errorMessage } = res
if (code === 200 && errorCode === 0) {
this.$toast('保存成功')
this.isShowAddressPopup = false
this.loadData()
} else {
this.$toast(errorMessage)
}
})
},
detailInfoTranslator(data) {
return {
title: data?.title || '',
drawPlay: data?.drawPlay || '',
drawTime: data?.drawTime || '',
prizeName: data?.prizeName || '',
prizeIcon: data?.prizeIcon || '',
exchangeType: +data?.exchangeType || 1,
address: data?.address || '',
contactPhone: data?.contactPhone || '',
code: data?.code || ''
}
}
}
}
</script>
<style lang="less" scoped>
.detail-popup {
padding-top: 54px;
label {
font-size: 14px;
font-weight: 500;
color: #666;
}
&__content {
padding: 0 20px;
}
&__info {
li {
display: flex;
margin-bottom: 15px;
&:last-child {
margin-bottom: 0;
}
span,
p {
font-size: 14px;
font-weight: 500;
}
}
}
&__info-img {
width: 84px;
height: 84px;
display: block;
border: 1px solid #dddddd;
margin-top: 5px;
border-radius: 6px;
}
&__code-info {
margin-top: 40px;
padding-top: 28px;
border-top: 1px dashed #dddd;
display: flex;
flex-direction: column;
align-items: center;
}
&__code-text {
height: 18px;
font-size: 12px;
font-weight: 500;
line-height: 18px;
display: flex;
margin: 10px 0;
}
&__notice {
color: #ff1111;
font-size: 12px;
}
&__edit-btn {
color: #2e7ff9;
}
}
</style>
const packageEnv = require('./package.json')
const protocol = process.client && window.location.protocol
const isHttps = protocol === 'https:'
export default {
defDomain: isHttps ? process.env.API_DOMAIN.replace('8680', '843') : process.env.API_DOMAIN,
noGateDomain: '//1812501212048408.cn-hangzhou.fc.aliyuncs.com/2016-08-15/proxy/gdyactivity.online',
goDomain: '//golivec.guangdianyun.tv/v1',
privateDomain: '//privateapi.guangdianyun.tv/v1',
activityDomain: '//activity.guangdianyun.tv/v1',
defBanner: `${process.env.OSS_DOMAIN}/common/img/lottery_banner_default.png`,
defShareImg: `${protocol || 'https:'}${process.env.OSS_DOMAIN}/common/img/lottery.png`, // 必须带协议头,否则微信分享配置时无法生效
env: process.env.X_CA_STAGE,
private: process.env.private,
ossImageServe: process.env.private ? '' : '?x-oss-process=style/mobilebackground',
tokenKey: 'token'
}
/* sentry */
export const sentryOptions = {
dsn: `${protocol}//f28c699ad832413c8b529520ea4b6df0@sentry.guangdianyun.tv/22`,
version: packageEnv.version,
env: process.env.X_CA_STAGE,
name: packageEnv.name
}
/* 谷歌统计 */
export const GTAG_ID = 'UA-162031360-6'
// arms
export const armsPid = 'huh7k89btk@1db1146a1ca55ff'
/* 正式:私有化部署(private)环境变量 */
module.exports = {
X_CA_STAGE: '',
API_DOMAIN: '//cgateway.cbnbn.cn/v1',
OSS_DOMAIN: '//static.cbnbn.cn',
ROP_DOMAIN: 'cbnbn.cn',
private: true,
privateType: 'cbn'
}
/* 正式:私有化部署(private)环境变量 */
module.exports = {
X_CA_STAGE: '',
API_DOMAIN: '//cgateway.hemeiyun.net:8680/v1',
OSS_DOMAIN: '//eos-wuxi-1.cmecloud.cn/g-resource',
ROP_DOMAIN: 'hemeiyun.net',
private: true,
privateType: 'cm'
}
/* 测试(dev)环境变量 */
module.exports = {
NODE_ENV: 'production',
X_CA_STAGE: 'TEST',
private: false,
run_server: 'development'
}
/* 写在该文件下的所有配置在所有环境下都会设置 且值可被覆盖 */
module.exports = {
baseUrl: '/lottery/',
API_DOMAIN: '//apiliveroom.dev.guangdianyun.tv/v1',
OSS_DOMAIN: '//static-pro.guangdianyun.tv',
ROP_DOMAIN: 'aodianyun.com',
X_CA_STAGE: 'TEST',
TOKEN_KEY: 'token',
private: false,
run_port: 4002,
}
/* 正式:私有化部署(private)环境变量 */
module.exports = {
X_CA_STAGE: '',
API_DOMAIN: '//cgateway.huaguangyun.cn/v1',
OSS_DOMAIN: '//g-resource.obs.cn-north-4.myhuaweicloud.com',
ROP_DOMAIN: 'huaguangyun.cn',
private: true,
privateType: 'huawei',
run_port: 34002,
}
/* 灰度(pre)环境变量 */
module.exports = {
NODE_ENV: 'production',
X_CA_STAGE: 'PRE',
private: false,
run_server: 'preview'
}
/* 正式(prod)环境变量 */
module.exports = {
NODE_ENV: 'production',
X_CA_STAGE: '',
private: false,
run_server: 'production'
}
/* 正式:私有化部署(private)环境变量 */
module.exports = {
X_CA_STAGE: '',
API_DOMAIN: '//cgateway.vvku.com/v1',
OSS_DOMAIN: '//static.vvku.com',
ROP_DOMAIN: 'vvku.com',
private: true,
privateType: 'vvku'
}
import Vue from 'vue'
// https://webpack.js.org/guides/dependency-management/#requirecontext
const modulesFiles = require.context('./modules', true, /\.js$/)
// you do not need `import app from './modules/app'`
// it will auto require all vuex module from modules file
modulesFiles.keys().forEach(modulePath => {
const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1')
const value = modulesFiles(modulePath)
Vue.filter(moduleName, value.default)
})
import dayjs from 'dayjs'
import 'dayjs/locale/zh-cn'
export default (time, format = 'YYYY-MM-DD HH:mm') => {
if (!time) return ''
return dayjs(time).locale('zh-cn').format(format)
}
/**
* 阿拉伯数字转中文数字,
* 如果传入数字时则最多处理到21位,超过21位js会自动将数字表示成科学计数法,导致精度丢失和处理出错
* 传入数字字符串则没有限制
* @param {number|string} digit
*/
export default (digit) => {
digit = typeof digit === 'number' ? String(digit) : digit;
const zh = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九'];
const unit = ['千', '百', '十', ''];
const quot = ['万', '亿', '兆', '京', '垓', '秭', '穰', '沟', '涧', '正', '载', '极', '恒河沙', '阿僧祗', '那由他', '不可思议', '无量', '大数'];
let breakLen = Math.ceil(digit.length / 4);
let notBreakSegment = digit.length % 4 || 4;
let segment;
let zeroFlag = [], allZeroFlag = [];
let result = '';
while (breakLen > 0) {
if (!result) { // 第一次执行
segment = digit.slice(0, notBreakSegment);
let segmentLen = segment.length;
for (let i = 0; i < segmentLen; i++) {
if (segment[i] != 0) {
if (zeroFlag.length > 0) {
result += '零' + zh[segment[i]] + unit[4 - segmentLen + i];
// 判断是否需要加上 quot 单位
if (i === segmentLen - 1 && breakLen > 1) {
result += quot[breakLen - 2];
}
zeroFlag.length = 0;
} else {
result += zh[segment[i]] + unit[4 - segmentLen + i];
if (i === segmentLen - 1 && breakLen > 1) {
result += quot[breakLen - 2];
}
if(result === '二'){
result = '两'
}
}
} else {
// 处理为 0 的情形
if (segmentLen == 1) {
result += zh[segment[i]];
break;
}
zeroFlag.push(segment[i]);
continue;
}
}
} else {
segment = digit.slice(notBreakSegment, notBreakSegment + 4);
notBreakSegment += 4;
for (let j = 0; j < segment.length; j++) {
if (segment[j] != 0) {
if (zeroFlag.length > 0) {
// 第一次执行zeroFlag长度不为0,说明上一个分区最后有0待处理
if (j === 0) {
result += quot[breakLen - 1] + zh[segment[j]] + unit[j];
} else {
result += '零' + zh[segment[j]] + unit[j];
}
zeroFlag.length = 0;
} else {
result += zh[segment[j]] + unit[j];
}
// 判断是否需要加上 quot 单位
if (j === segment.length - 1 && breakLen > 1) {
result += quot[breakLen - 2];
}
} else {
// 第一次执行如果zeroFlag长度不为0, 且上一划分不全为0
if (j === 0 && zeroFlag.length > 0 && allZeroFlag.length === 0) {
result += quot[breakLen - 1];
zeroFlag.length = 0;
zeroFlag.push(segment[j]);
} else if (allZeroFlag.length > 0) {
// 执行到最后
if (breakLen == 1) {
result += '';
} else {
zeroFlag.length = 0;
}
} else {
zeroFlag.push(segment[j]);
}
if (j === segment.length - 1 && zeroFlag.length === 4 && breakLen !== 1) {
// 如果执行到末尾
if (breakLen === 1) {
allZeroFlag.length = 0;
zeroFlag.length = 0;
result += quot[breakLen - 1];
} else {
allZeroFlag.push(segment[j]);
}
}
continue;
}
}
--breakLen;
}
return result;
}
}
FROM alpine AS builder
WORKDIR /home/app
RUN apk add --no-cache --update nodejs yarn
COPY package.json yarn.lock ./
RUN yarn install --registry=https://registry.yarnpkg.com
FROM registry.cn-hangzhou.aliyuncs.com/open_images/node12.13.1-pm2
ADD ./ /var/www/web_lottery
ENV HOST 0.0.0.0
ENV TZ Asia/Shanghai
WORKDIR /var/www/web_lottery
COPY --from=builder /home/app/package.json ./package.json
COPY --from=builder /home/app/node_modules ./node_modules
RUN yarn build:huawei
RUN rm -rf assets components layouts middleware pages plugins store .eslintrc.js .gitignore build.yml Dockerfile README.md start.sh
EXPOSE 34002
RUN chmod +x huaweiStart.sh
ENTRYPOINT ["./huaweiStart.sh"]
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment