This commit is contained in:
zpc 2025-05-15 15:33:32 +08:00
parent ff56a97f34
commit 47899ec619
32 changed files with 4702 additions and 412 deletions

View File

@ -12,283 +12,431 @@ const wx_version = "110";
// 白名单页面(不需要登录即可访问)
export const whiteList = [
"pages/shouye/index", // 首页
"pages/shouye/detail", // 详情页
"pages/shouye/huanxiang", // 换箱页面
"pages/mall/index", // 商城首页
"pages/shouye/detail_wuxian", // 无限详情页
"pages/sangdai/sangdai", // 盒柜页面
"pages/infinite/index", // 福利首页
"pages/user/index", // 用户中心
"pages/infinite/daily_check_in", // 每日签到
"pages/infinite/bonus_house", // 福利屋
"pages/other/prize_draw", // 每日奖品抽取
"pages/shouye/danye", // 单页
"pages/guize/guize", // 规则页面
"pages/shouye/dada_ranking", // 达达排行榜
"pages/shouye/yaoqing_ranking", // 邀请排行榜
"pages/user/login" // 登录页面
"pages/shouye/index", // 首页
"pages/shouye/detail", // 详情页
"pages/shouye/huanxiang", // 换箱页面
"pages/mall/index", // 商城首页
"pages/shouye/detail_wuxian", // 无限详情页
"pages/sangdai/sangdai", // 盒柜页面
"pages/infinite/index", // 福利首页
"pages/user/index", // 用户中心
"pages/infinite/daily_check_in", // 每日签到
"pages/infinite/bonus_house", // 福利屋
"pages/other/prize_draw", // 每日奖品抽取
"pages/shouye/danye", // 单页
"pages/guize/guize", // 规则页面
"pages/shouye/dada_ranking", // 达达排行榜
"pages/shouye/yaoqing_ranking", // 邀请排行榜
"pages/user/login", // 登录页面
"pages/shouye/slots"
];
// API白名单不需要登录即可访问的API
export const apiWhiteList = [
'login_record',
'api/warehouse_index',
'api/user',
'api/warehouse_send_record'
'login_record',
'api/warehouse_index',
'api/user',
'api/warehouse_send_record'
];
const defaultConfig = {
"good_type": [{ "value": 0, "sort_order": 0, "is_show": 1, "name": "全部", "pay_wechat": 1, "pay_balance": 1, "pay_currency": 1, "pay_currency2": 1, "pay_coupon": 1, "is_deduction": 1 }, { "value": 1, "sort_order": 1, "is_show": 0, "name": "一番赏", "pay_wechat": 1, "pay_balance": 1, "pay_currency": 1, "pay_currency2": 1, "pay_coupon": 1, "is_deduction": 1 }, { "value": 2, "sort_order": 2, "is_show": 1, "name": "无限赏", "pay_wechat": 1, "pay_balance": 1, "pay_currency": 1, "pay_currency2": 1, "pay_coupon": 1, "is_deduction": 1 }, { "value": 3, "sort_order": 3, "is_show": 0, "name": "擂台赏", "pay_wechat": 1, "pay_balance": 1, "pay_currency": 1, "pay_currency2": 1, "pay_coupon": 1, "is_deduction": 1 }, { "value": 5, "sort_order": 4, "is_show": 0, "name": "积分赏", "pay_wechat": 0, "pay_balance": 0, "pay_currency": 0, "pay_currency2": 1, "pay_coupon": 1, "is_deduction": 0 }, { "value": 6, "sort_order": 5, "is_show": 1, "name": "限时活动", "pay_wechat": 1, "pay_balance": 1, "pay_currency": 1, "pay_currency2": 1, "pay_coupon": 1, "is_deduction": 1 }, { "value": 8, "sort_order": 6, "is_show": 1, "name": "领主赏", "pay_wechat": 1, "pay_balance": 1, "pay_currency": 1, "pay_currency2": 1, "pay_coupon": 1, "is_deduction": 1 }, { "value": 9, "sort_order": 7, "is_show": 0, "name": "连击赏", "pay_wechat": 1, "pay_balance": 1, "pay_currency": 1, "pay_currency2": 1, "pay_coupon": 1, "is_deduction": 1 }, { "value": 10, "sort_order": 8, "is_show": 0, "name": "商城赏", "pay_wechat": 1, "pay_balance": 0, "pay_currency": 0, "pay_currency2": 1, "pay_coupon": 0, "is_deduction": 0 }, { "value": 11, "sort_order": 9, "is_show": 0, "name": "自制赏", "pay_wechat": 1, "pay_balance": 1, "pay_currency": 1, "pay_currency2": 1, "pay_coupon": 1, "is_deduction": 1 }, { "value": 15, "sort_order": 15, "is_show": 0, "name": "福利屋", "pay_wechat": 1, "pay_balance": 1, "pay_currency": 0, "pay_currency2": 0, "pay_coupon": 0, "is_deduction": 0 }, { "value": 16, "sort_order": 16, "is_show": 1, "name": "翻倍赏", "pay_wechat": 1, "pay_balance": 1, "pay_currency": 1, "pay_currency2": 1, "pay_coupon": 1, "is_deduction": 1 }, { "value": 17, "sort_order": 17, "is_show": 0, "name": "外卖盒子", "pay_wechat": 1, "pay_balance": 1, "pay_currency": 0, "pay_currency2": 0, "pay_coupon": 0, "is_deduction": 0 }],
"app_setting": {
"key": "app_setting",
"app_name": "友达赏",
"purchase_popup": "1",
"exchange_times": "2",
"balance_name": "钻石",
"balance_icon": "https://image.zfunbox.cn/app/icons/20250412/a482b527477e74f8a18ec02ebc7f0b4e.png",
"good_type": [{
"value": 0,
"sort_order": 0,
"is_show": 1,
"name": "全部",
"pay_wechat": 1,
"pay_balance": 1,
"pay_currency": 1,
"pay_currency2": 1,
"pay_coupon": 1,
"is_deduction": 1
}, {
"value": 1,
"sort_order": 1,
"is_show": 0,
"name": "一番赏",
"pay_wechat": 1,
"pay_balance": 1,
"pay_currency": 1,
"pay_currency2": 1,
"pay_coupon": 1,
"is_deduction": 1
}, {
"value": 2,
"sort_order": 2,
"is_show": 1,
"name": "无限赏",
"pay_wechat": 1,
"pay_balance": 1,
"pay_currency": 1,
"pay_currency2": 1,
"pay_coupon": 1,
"is_deduction": 1
}, {
"value": 3,
"sort_order": 3,
"is_show": 0,
"name": "擂台赏",
"pay_wechat": 1,
"pay_balance": 1,
"pay_currency": 1,
"pay_currency2": 1,
"pay_coupon": 1,
"is_deduction": 1
}, {
"value": 5,
"sort_order": 4,
"is_show": 0,
"name": "积分赏",
"pay_wechat": 0,
"pay_balance": 0,
"pay_currency": 0,
"pay_currency2": 1,
"pay_coupon": 1,
"is_deduction": 0
}, {
"value": 6,
"sort_order": 5,
"is_show": 1,
"name": "限时活动",
"pay_wechat": 1,
"pay_balance": 1,
"pay_currency": 1,
"pay_currency2": 1,
"pay_coupon": 1,
"is_deduction": 1
}, {
"value": 8,
"sort_order": 6,
"is_show": 1,
"name": "领主赏",
"pay_wechat": 1,
"pay_balance": 1,
"pay_currency": 1,
"pay_currency2": 1,
"pay_coupon": 1,
"is_deduction": 1
}, {
"value": 9,
"sort_order": 7,
"is_show": 0,
"name": "连击赏",
"pay_wechat": 1,
"pay_balance": 1,
"pay_currency": 1,
"pay_currency2": 1,
"pay_coupon": 1,
"is_deduction": 1
}, {
"value": 10,
"sort_order": 8,
"is_show": 0,
"name": "商城赏",
"pay_wechat": 1,
"pay_balance": 0,
"pay_currency": 0,
"pay_currency2": 1,
"pay_coupon": 0,
"is_deduction": 0
}, {
"value": 11,
"sort_order": 9,
"is_show": 0,
"name": "自制赏",
"pay_wechat": 1,
"pay_balance": 1,
"pay_currency": 1,
"pay_currency2": 1,
"pay_coupon": 1,
"is_deduction": 1
}, {
"value": 15,
"sort_order": 15,
"is_show": 0,
"name": "福利屋",
"pay_wechat": 1,
"pay_balance": 1,
"pay_currency": 0,
"pay_currency2": 0,
"pay_coupon": 0,
"is_deduction": 0
}, {
"value": 16,
"sort_order": 16,
"is_show": 1,
"name": "翻倍赏",
"pay_wechat": 1,
"pay_balance": 1,
"pay_currency": 1,
"pay_currency2": 1,
"pay_coupon": 1,
"is_deduction": 1
}, {
"value": 17,
"sort_order": 17,
"is_show": 0,
"name": "外卖盒子",
"pay_wechat": 1,
"pay_balance": 1,
"pay_currency": 0,
"pay_currency2": 0,
"pay_coupon": 0,
"is_deduction": 0
}],
"app_setting": {
"key": "app_setting",
"app_name": "友达赏",
"purchase_popup": "1",
"exchange_times": "2",
"balance_name": "钻石",
"balance_icon": "https://image.zfunbox.cn/app/icons/20250412/a482b527477e74f8a18ec02ebc7f0b4e.png",
"currency1_name": "UU币",
"currency1_icon": "https://image.zfunbox.cn/app/icons/20250412/3d1741965e9439372d1ce101bd110616.png",
"currency2_name": "达达券",
"currency2_icon": "https://image.zfunbox.cn/app/icons/20250412/19a9f69011fd82c85ab0df3a064a309c.png",
"win_audio": "https://image.zfunbox.cn/app/20250407/14ba53d367e1d131a344c6fd5cc0e28e.mp3",
"applet_version": "v1.0.3",
"sign_in_spend_limit": "1",
"show_dadajuan_limit": "0",
"waimai_box_id": "1049",
"daily_free_draw_id": "1050",
"cabinet_exchange_limit": "0",
"daily_coupon_limit": "0",
"update_time": 1745379501
},
version: wx_version
"currency1_name": "UU币",
"currency1_icon": "https://image.zfunbox.cn/app/icons/20250412/3d1741965e9439372d1ce101bd110616.png",
"currency2_name": "达达券",
"currency2_icon": "https://image.zfunbox.cn/app/icons/20250412/19a9f69011fd82c85ab0df3a064a309c.png",
"win_audio": "https://image.zfunbox.cn/app/20250407/14ba53d367e1d131a344c6fd5cc0e28e.mp3",
"applet_version": "v1.0.3",
"sign_in_spend_limit": "1",
"show_dadajuan_limit": "0",
"waimai_box_id": "1049",
"daily_free_draw_id": "1050",
"cabinet_exchange_limit": "0",
"daily_coupon_limit": "0",
"update_time": 1745379501
},
version: wx_version
};
// 配置类
class ConfigManager {
static getShareImageUrl() {
return "https://image.zfunbox.cn/icon/fenxiang.jpg";//this.getAppSetting('share_image_url');
}
/**
* 初始化并加载配置
* 在应用启动时调用
*/
static init() {
return this.loadConfig();
}
static getShareImageUrl() {
return "https://image.zfunbox.cn/icon/fenxiang.jpg"; //this.getAppSetting('share_image_url');
}
/**
* 初始化并加载配置
* 在应用启动时调用
*/
static init() {
return this.loadConfig();
}
/**
* 加载配置数据
* @returns {Promise} 返回加载完成的Promise
*/
static loadConfig() {
// 避免重复加载
if (isLoading) {
return loadPromise;
}
/**
* 加载配置数据
* @returns {Promise} 返回加载完成的Promise
*/
static loadConfig() {
// 避免重复加载
if (isLoading) {
return loadPromise;
}
isLoading = true;
loadPromise = new Promise((resolve, reject) => {
RequestManager.get('config')
.then(res => {
if (res.status === 1 && res.data) {
configData = res.data;
console.log('全局配置数据加载成功');
let _configData = uni.getStorageSync("configData");
uni.setStorageSync('configData', configData);
if (_configData != null && _configData != '') {
if (_configData.app_setting.applet_version != configData.app_setting.applet_version) {
console.log('版本号不一致,需要更新');
const updateManager = uni.getUpdateManager();
uni.showModal({
title: "更新提示",
content: "新版本已经准备好,是否重启应用?",
showCancel: false,
success(res) {
if (res.confirm) {
console.log('开始重启');
wx.restartMiniProgram({ path: "/pages/shouye/index" });
// 新的版本已经下载好,调用 applyUpdate 应用新版本并重启
//updateManager.applyUpdate();
isLoading = true;
loadPromise = new Promise((resolve, reject) => {
RequestManager.get('config')
.then(res => {
if (res.status === 1 && res.data) {
configData = res.data;
console.log('全局配置数据加载成功');
let _configData = uni.getStorageSync("configData");
uni.setStorageSync('configData', configData);
if (_configData != null && _configData != '') {
if (_configData.app_setting.applet_version != configData.app_setting
.applet_version) {
console.log('版本号不一致,需要更新');
const updateManager = uni.getUpdateManager();
uni.showModal({
title: "更新提示",
content: "新版本已经准备好,是否重启应用?",
showCancel: false,
success(res) {
if (res.confirm) {
console.log('开始重启');
wx.restartMiniProgram({
path: "/pages/shouye/index"
});
// 新的版本已经下载好,调用 applyUpdate 应用新版本并重启
//updateManager.applyUpdate();
//#ifdef MP-WEIXIN
//#ifdef MP-WEIXIN
//#endif
}
},
});
}
}
resolve(configData);
} else {
console.error('加载配置数据失败:', res.msg || '未知错误');
reject(new Error(res.msg || '加载配置失败'));
}
})
.catch(err => {
console.error('请求配置数据失败:', err);
isLoading = false;
reject(err);
});
});
//#endif
}
},
});
}
}
resolve(configData);
} else {
console.error('加载配置数据失败:', res.msg || '未知错误');
reject(new Error(res.msg || '加载配置失败'));
}
})
.catch(err => {
console.error('请求配置数据失败:', err);
isLoading = false;
reject(err);
});
});
return loadPromise;
}
return loadPromise;
}
/**
* 获取所有配置数据
* @returns {Object} 配置数据对象
*/
static getAll() {
if (configData) {
return configData;
} else {
console.warn('配置数据尚未加载,获取本地缓存中。。。');
configData = uni.getStorageSync("configData");
if (configData != null) {
return configData;
}
this.loadConfig();
return {};
}
}
/**
* 获取所有配置数据
* @returns {Object} 配置数据对象
*/
static getAll() {
if (configData) {
return configData;
} else {
console.warn('配置数据尚未加载,获取本地缓存中。。。');
configData = uni.getStorageSync("configData");
if (configData != null) {
return configData;
}
this.loadConfig();
return {};
}
}
/**
* 获取指定键的配置值
* @param {String} key 配置键
* @param {any} defaultValue 默认值当键不存在时返回
* @returns {any} 配置值
*/
static get(key) {
if (!configData) {
console.warn('配置数据尚未加载,获取本地缓存中。。。');
configData = uni.getStorageSync("configData");
if (configData != null && configData != "") {
// 不直接返回configData继续执行下面的代码
} else {
configData = defaultConfig;
}
/**
* 获取指定键的配置值
* @param {String} key 配置键
* @param {any} defaultValue 默认值当键不存在时返回
* @returns {any} 配置值
*/
static get(key) {
if (!configData) {
console.warn('配置数据尚未加载,获取本地缓存中。。。');
configData = uni.getStorageSync("configData");
if (configData != null && configData != "") {
// 不直接返回configData继续执行下面的代码
} else {
configData = defaultConfig;
}
}
return key in configData ? configData[key] : defaultConfig[key];
}
}
return key in configData ? configData[key] : defaultConfig[key];
}
/**
* 盒子类型
* @returns {Object} 商品类型对象
*/
static getGoodType() {
let goodType = this.get('good_type');
if (goodType != null) {
if (this.GetVersion()) {
return goodType.filter(item => item.is_show === 1 && (item.value === 2 || item.value === 0)).map(item => {
return {
id: item.value,
title: item.name
}
});
}
return goodType.filter(item => item.is_show === 1).map(item => {
// console.log(item);
/**
* 盒子类型
* @returns {Object} 商品类型对象
*/
static getGoodType() {
let goodType = this.get('good_type');
if (goodType != null) {
if (this.GetVersion()) {
return goodType.filter(item => item.is_show === 1 && (item.value === 2 || item.value === 0)).map(
item => {
return {
id: item.value,
title: item.name
}
});
}
return goodType.filter(item => item.is_show === 1).map(item => {
// console.log(item);
return {
id: item.value,
title: item.name
}
});
}
return [];
}
return {
id: item.value,
title: item.name
}
});
}
return [];
}
/**
* 获取指定盒子类型
* @param {Number} type 盒子类型
* @returns {Object} 盒子类型对象
*/
static getGoodTypeFind(type) {
let goodType = this.get('good_type');
if (goodType != null) {
return goodType.find(item => item.value == type);
}
}
/**
* 获取指定盒子类型
* @param {Number} type 盒子类型
* @returns {Object} 盒子类型对象
*/
static getGoodTypeFind(type) {
let goodType = this.get('good_type');
if (goodType != null) {
return goodType.find(item => item.value == type);
}
}
/**
* 刷新配置数据
* @returns {Promise} 返回刷新完成的Promise
*/
static refresh() {
isLoading = false;
return this.loadConfig();
}
/**
* 刷新配置数据
* @returns {Promise} 返回刷新完成的Promise
*/
static refresh() {
isLoading = false;
return this.loadConfig();
}
/**
* 检查配置是否已加载
* @returns {Boolean} 是否已加载
*/
static isLoaded() {
return configData !== null;
}
/**
* 检查配置是否已加载
* @returns {Boolean} 是否已加载
*/
static isLoaded() {
return configData !== null;
}
/**
* 获取应用设置
* @param {String} key 设置键
* @returns {Object|String|null} 设置值
*/
static getAppSetting(key = null) {
let appSetting = this.get('app_setting');
if (key == null) {
return appSetting;
}
return key in appSetting ? appSetting[key] : null;
}
/**
* 获取应用设置
* @param {String} key 设置键
* @returns {Object|String|null} 设置值
*/
static getAppSetting(key = null) {
let appSetting = this.get('app_setting');
if (key == null) {
return appSetting;
}
return key in appSetting ? appSetting[key] : null;
}
/**
* 获取指定键的配置值
* @param {String} key 配置键
* @param {any} defaultValue 默认值当键不存在时返回
* @returns {any} 配置值
*/
static async getAsync(key, defaultValue = null) {
if (!configData) {
// console.warn('配置数据尚未加载,正在加载中...');
await this.loadConfig();
if (configData) {
return key in configData ? configData[key] : defaultValue;;
}
return defaultValue;
}
/**
* 获取指定键的配置值
* @param {String} key 配置键
* @param {any} defaultValue 默认值当键不存在时返回
* @returns {any} 配置值
*/
static async getAsync(key, defaultValue = null) {
if (!configData) {
// console.warn('配置数据尚未加载,正在加载中...');
await this.loadConfig();
if (configData) {
return key in configData ? configData[key] : defaultValue;;
}
return defaultValue;
}
return key in configData ? configData[key] : defaultValue;
}
return key in configData ? configData[key] : defaultValue;
}
/**
* 获取应用设置
* @param {String} key 设置键
* @returns {Object|String|null} 设置值
*/
static async getAppSettingAsync(key = null) {
let appSetting = await this.getAsync('app_setting');
if (key == null) {
return appSetting;
}
return key in appSetting ? appSetting[key] : null;
}
/**
* 获取应用设置
* @param {String} key 设置键
* @returns {Object|String|null} 设置值
*/
static async getAppSettingAsync(key = null) {
let appSetting = await this.getAsync('app_setting');
if (key == null) {
return appSetting;
}
return key in appSetting ? appSetting[key] : null;
}
/**
* 获取版本号
* @returns {Boolean} 是否为微信版本
*/
static GetVersion() {
//#ifdef MP-WEIXIN
let version = this.get('version');
if (version == wx_version) {
return true;
}
return false;
//#endif
return false;
}
/**
* 获取版本号
* @returns {Boolean} 是否为微信版本
*/
static GetVersion() {
//#ifdef MP-WEIXIN
let version = this.get('version');
if (version == wx_version) {
return true;
}
return false;
//#endif
return false;
}
}
export default ConfigManager;
export default ConfigManager;

BIN
components/.DS_Store vendored

Binary file not shown.

BIN
components/@lucky-canvas/.DS_Store vendored Normal file

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,689 @@
<template>
<view class="canvas-prize-wheel">
<!-- 绿色背景 -->
<view class="prize-strip-background"></view>
<canvas
id="prizeCanvas"
ref="prizeCanvas"
canvas-id="prizeCanvas"
:style="canvasStyle"
@touchstart="onTouchStart"
@touchmove="onTouchMove"
@touchend="onTouchEnd">
</canvas>
<!-- 中心指示器 -->
<view class="center-indicator"></view>
</view>
</template>
<script>
export default {
name: 'CanvasPrizeWheel',
props: {
//
prizes: {
type: Array,
default: () => []
},
// ()
duration: {
type: Number,
default: 4
},
//
itemWidth: {
type: Number,
default: () => uni.upx2px(180)
},
//
itemHeight: {
type: Number,
default: () => uni.upx2px(150)
},
//
maxSpeed: {
type: Number,
default: 2000
},
//
allowDrag: {
type: Boolean,
default: false
},
//
backgroundColor: {
type: String,
default: 'rgba(255, 255, 255, 0.8)'
},
//
highlightColor: {
type: String,
default: 'rgba(255, 215, 0, 0.3)'
},
//
highlightShadow: {
type: String,
default: 'rgba(255, 215, 0, 0.5)'
},
//
borderRadius: {
type: Number,
default: 8
}
},
data() {
return {
canvas: null, // Canvas
ctx: null, //
canvasWidth: 0, // Canvas
canvasHeight: 0, // Canvas
isSpinning: false, //
activeIndex: -1, //
currentOffset: 0, //
targetPrize: null, //
targetIndex: -1, //
speedPhase: 'initial', //
animationId: null, // ID
lastTimestamp: 0, //
speed: 0, // (px/s)
dragInfo: { //
isDragging: false,
startX: 0,
lastX: 0,
startOffset: 0
},
pixelRatio: 1, //
itemsCount: 0, //
repeatCount: 1, //
isInitialized: false, //
}
},
computed: {
canvasStyle() {
return {
width: '100%',
height: `${this.itemHeight}px`,
zIndex: 2
}
},
//
centerOffset() {
return this.canvasWidth / 2 - this.itemWidth / 2
},
// ()
cycleWidth() {
return this.itemWidth * this.prizes.length
}
},
mounted() {
// DOM
this.$nextTick(() => {
setTimeout(() => {
this.initCanvas()
this.itemsCount = this.prizes.length
}, 100)
})
//
window.addEventListener('resize', this.onResize)
},
beforeDestroy() {
//
this.stopAnimation()
window.removeEventListener('resize', this.onResize)
},
methods: {
// Canvas
async initCanvas() {
try {
// canvas
const query = uni.createSelectorQuery().in(this)
await new Promise(resolve => {
query.select('#prizeCanvas')
.fields({ node: true, size: true, rect: true, context: true })
.exec(res => {
console.log('Canvas查询结果:', res)
if (!res || !res[0]) {
console.warn('无法获取Canvas节点使用降级方案')
this.initCanvasContext()
resolve()
return
}
// Canvas2D
if (res[0].node) {
const canvas = res[0].node
const ctx = canvas.getContext('2d')
//
this.pixelRatio = uni.getSystemInfoSync().pixelRatio || 1
console.log('设备像素比:', this.pixelRatio)
// canvas
this.canvasWidth = res[0].width || uni.getSystemInfoSync().windowWidth
this.canvasHeight = this.itemHeight
console.log('Canvas尺寸:', this.canvasWidth, this.canvasHeight)
// Canvas
canvas.width = this.canvasWidth * this.pixelRatio
canvas.height = this.canvasHeight * this.pixelRatio
//
ctx.scale(this.pixelRatio, this.pixelRatio)
this.canvas = canvas
this.ctx = ctx
}
// 使Canvas API
else if (res[0].context) {
this.ctx = res[0].context
this.canvasWidth = res[0].width || uni.getSystemInfoSync().windowWidth
this.canvasHeight = this.itemHeight
}
//
this.resetPosition()
//
this.isInitialized = true
this.render()
resolve()
})
})
} catch (error) {
console.error('初始化Canvas失败:', error)
this.initCanvasContext() //
}
},
// 使2D
initCanvasContext() {
console.log('使用降级Canvas API')
const sysInfo = uni.getSystemInfoSync()
this.canvasWidth = sysInfo.windowWidth
this.canvasHeight = this.itemHeight
this.pixelRatio = sysInfo.pixelRatio || 1
//
this.ctx = uni.createCanvasContext('prizeCanvas', this)
//
this.resetPosition()
//
this.isInitialized = true
this.render()
},
//
onResize() {
// Canvas
this.initCanvas()
},
//
resetPosition() {
//
this.currentOffset = this.centerOffset - this.cycleWidth
},
//
startSpin() {
if (this.isSpinning) return
this.isSpinning = true
this.speedPhase = 'accelerating'
this.speed = 30 //
this.targetPrize = null
this.targetIndex = -1
this.lastTimestamp = performance.now()
//
this.$emit('spin-start')
//
this.animationId = requestAnimationFrame(this.animate)
},
//
setPrize(prize) {
if (!this.isSpinning) return
//
this.targetPrize = prize
//
this.targetIndex = this.prizes.findIndex(item =>
(item.id && item.id === prize.id) ||
(item.value && item.value === prize.value)
)
if (this.targetIndex === -1) {
console.warn('目标奖品不在奖品列表中')
// 使
this.targetIndex = 0
}
//
setTimeout(() => {
if (this.isSpinning) {
this.speedPhase = 'decelerating'
}
}, 1000) //
},
//
animate(timestamp) {
if (!this.isSpinning) return
//
const delta = timestamp - this.lastTimestamp
this.lastTimestamp = timestamp
//
switch(this.speedPhase) {
case 'accelerating':
//
this.speed = Math.min(this.maxSpeed, this.speed * 1.08)
if (this.speed >= this.maxSpeed) {
this.speedPhase = 'constant'
}
break
case 'decelerating':
//
this.speed *= 0.97
//
if (this.speed < 200 && this.targetIndex !== -1) {
this.speedPhase = 'stopping'
this.prepareToStop()
}
break
case 'stopping':
// -
this.speed *= 0.85
//
if (this.speed < 10) {
this.finalizePosition()
this.stopAnimation()
return
}
break
}
//
this.moveStrip(delta)
//
this.render()
//
this.animationId = requestAnimationFrame(this.animate)
},
//
moveStrip(deltaTime) {
//
const distance = (this.speed * deltaTime) / 1000
//
this.currentOffset -= distance
//
this.checkResetPosition()
//
this.updateActiveItem()
},
//
checkResetPosition() {
//
if (Math.abs(this.currentOffset) > this.cycleWidth * 3) {
//
const remainder = this.currentOffset % this.cycleWidth
//
this.currentOffset = this.centerOffset - this.cycleWidth + remainder
}
},
//
updateActiveItem() {
//
const relativePos = -this.currentOffset + this.centerOffset
//
const cyclePos = (relativePos % this.cycleWidth + this.cycleWidth) % this.cycleWidth
//
const itemIndex = Math.floor(cyclePos / this.itemWidth)
if (itemIndex >= 0 && itemIndex < this.prizes.length) {
this.activeIndex = itemIndex
} else {
this.activeIndex = -1
}
},
//
prepareToStop() {
if (this.targetIndex === -1) return
//
const relativePos = -this.currentOffset + this.centerOffset
//
const cyclePos = (relativePos % this.cycleWidth + this.cycleWidth) % this.cycleWidth
//
const currentItemIndex = Math.floor(cyclePos / this.itemWidth)
// -
const decelerationFactor = 0.95
//
//
let stepsToTarget = this.targetIndex - currentItemIndex
if (stepsToTarget <= 0) {
stepsToTarget += this.prizes.length
}
//
if (this.speed > 100) {
this.speed = Math.max(100, this.speed * decelerationFactor)
}
},
//
finalizePosition() {
if (this.targetIndex === -1) return
//
const relativePos = -this.currentOffset + this.centerOffset
const cyclePos = (relativePos % this.cycleWidth + this.cycleWidth) % this.cycleWidth
const currentItemIndex = Math.floor(cyclePos / this.itemWidth)
//
let adjustedOffset = this.currentOffset
//
if (Math.abs(currentItemIndex - this.targetIndex) <= this.prizes.length / 2) {
const delta = (currentItemIndex - this.targetIndex) * this.itemWidth
adjustedOffset += delta
}
//
this.currentOffset = adjustedOffset
this.activeIndex = this.targetIndex
//
this.render()
},
//
stopAnimation() {
if (!this.isSpinning && !this.animationId) return
cancelAnimationFrame(this.animationId)
this.animationId = null
this.isSpinning = false
//
this.$emit('spin-end', this.targetPrize || this.prizes[this.activeIndex])
},
//
onTouchStart(e) {
if (!this.allowDrag || this.isSpinning) return
this.dragInfo.isDragging = true
this.dragInfo.startX = e.touches[0].clientX
this.dragInfo.lastX = this.dragInfo.startX
this.dragInfo.startOffset = this.currentOffset
},
//
onTouchMove(e) {
if (!this.dragInfo.isDragging) return
const x = e.touches[0].clientX
const deltaX = x - this.dragInfo.lastX
//
this.currentOffset += deltaX
//
this.dragInfo.lastX = x
//
this.updateActiveItem()
//
this.render()
},
//
onTouchEnd() {
if (!this.dragInfo.isDragging) return
this.dragInfo.isDragging = false
//
let momentum = (this.dragInfo.lastX - this.dragInfo.startX) / 10
//
const decelerate = () => {
if (Math.abs(momentum) < 0.1) return
momentum *= 0.95
this.currentOffset += momentum
//
this.updateActiveItem()
//
this.render()
requestAnimationFrame(decelerate)
}
requestAnimationFrame(decelerate)
},
//
render() {
if (!this.ctx || !this.isInitialized) return
try {
//
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
//
this.renderVisibleItems()
// Canvas
if (!this.canvas) {
this.ctx.draw()
}
} catch (error) {
console.error('渲染Canvas失败:', error)
}
},
//
renderVisibleItems() {
try {
//
const startX = -this.currentOffset
const endX = startX + this.canvasWidth
//
const cycleWidth = this.cycleWidth
const firstVisibleIndex = Math.floor(startX / this.itemWidth) - 1
const lastVisibleIndex = Math.ceil(endX / this.itemWidth) + 1
//
for (let i = firstVisibleIndex; i <= lastVisibleIndex; i++) {
//
const realIndex = ((i % this.prizes.length) + this.prizes.length) % this.prizes.length
const item = this.prizes[realIndex]
// x
const x = this.currentOffset + (i * this.itemWidth)
//
this.renderItem(item, x, realIndex === this.activeIndex)
}
} catch (error) {
console.error('渲染项目失败:', error)
}
},
//
renderItem(item, x, isActive) {
if (!item) return
try {
//
this.ctx.save()
//
const y = 0
const width = this.itemWidth
const height = this.itemHeight
const padding = Math.min(width, height) * 0.05
//
this.ctx.fillStyle = isActive ? this.highlightColor : this.backgroundColor
this.ctx.beginPath()
this.roundRect(
x + padding,
y + padding,
width - padding * 2,
height - padding * 2,
this.borderRadius
)
this.ctx.fill()
//
if (isActive) {
this.ctx.shadowColor = this.highlightShadow
this.ctx.shadowBlur = 10
}
//
const text = item.value || ''
this.ctx.fillStyle = item.color || '#000000'
this.ctx.font = 'bold 16px Arial'
this.ctx.textAlign = 'center'
this.ctx.textBaseline = 'middle'
this.ctx.fillText(text, x + width / 2, y + height / 2)
this.ctx.restore()
} catch (error) {
console.error('渲染奖品项失败:', error)
}
},
//
roundRect(x, y, width, height, radius) {
const r = Math.min(radius, width / 2, height / 2)
this.ctx.moveTo(x + r, y)
this.ctx.lineTo(x + width - r, y)
this.ctx.arcTo(x + width, y, x + width, y + r, r)
this.ctx.lineTo(x + width, y + height - r)
this.ctx.arcTo(x + width, y + height, x + width - r, y + height, r)
this.ctx.lineTo(x + r, y + height)
this.ctx.arcTo(x, y + height, x, y + height - r, r)
this.ctx.lineTo(x, y + r)
this.ctx.arcTo(x, y, x + r, y, r)
},
// Canvas
async exportImage() {
if (!this.canvas) {
// Canvas使uni.canvasToTempFilePath
return new Promise((resolve, reject) => {
uni.canvasToTempFilePath({
canvasId: 'prizeCanvas',
success: res => resolve(res.tempFilePath),
fail: err => reject(err)
}, this)
})
} else {
// Canvas使toDataURL
return this.canvas.toDataURL('image/png')
}
}
}
}
</script>
<style lang="scss">
.canvas-prize-wheel {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
.prize-strip-background {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #00a86b; /* 绿色背景 */
z-index: 1;
}
canvas {
position: relative;
z-index: 2;
}
.center-indicator {
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 2px;
height: 100%;
background-color: #ff5a5f;
z-index: 10;
pointer-events: none;
&:before {
content: '';
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 0;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-top: 10px solid #ff5a5f;
}
&:after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 0;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-bottom: 10px solid #ff5a5f;
}
}
}
</style>

View File

@ -0,0 +1,7 @@
<template></template>
<script>
</script>
<style lang="scss" scoped>
</style>

View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

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

752
components/lucky-canvas/types/index.d.ts vendored Normal file
View File

@ -0,0 +1,752 @@
declare type FontItemType = {
text: string;
top?: string | number;
left?: string | number;
fontColor?: string;
fontSize?: string;
fontStyle?: string;
fontWeight?: string;
lineHeight?: string;
};
declare type FontExtendType = {
wordWrap?: boolean;
lengthLimit?: string | number;
lineClamp?: number;
};
declare type ImgType = HTMLImageElement | HTMLCanvasElement;
declare type ImgItemType = {
src: string;
top?: string | number;
left?: string | number;
width?: string;
height?: string;
formatter?: (img: ImgType) => ImgType;
$resolve?: Function;
$reject?: Function;
};
declare type BorderRadiusType = string | number;
declare type BackgroundType = string;
declare type ShadowType = string;
declare type ConfigType = {
nodeType?: number;
flag: 'WEB' | 'MP-WX' | 'UNI-H5' | 'UNI-MP' | 'TARO-H5' | 'TARO-MP';
el?: string;
divElement?: HTMLDivElement;
canvasElement?: HTMLCanvasElement;
ctx: CanvasRenderingContext2D;
dpr: number;
handleCssUnit?: (num: number, unit: string) => number;
rAF?: Function;
setTimeout: Function;
setInterval: Function;
clearTimeout: Function;
clearInterval: Function;
beforeCreate?: Function;
beforeResize?: Function;
afterResize?: Function;
beforeInit?: Function;
afterInit?: Function;
beforeDraw?: Function;
afterDraw?: Function;
afterStart?: Function;
};
declare type RequireKey = 'width' | 'height';
declare type UserConfigType = Partial<Omit<ConfigType, RequireKey>> & Required<Pick<ConfigType, RequireKey>>;
declare type Tuple<T, Len extends number, Res extends T[] = []> = Res['length'] extends Len ? Res : Tuple<T, Len, [...Res, T]>;
interface WatchOptType {
handler?: () => Function;
immediate?: boolean;
deep?: boolean;
}
declare class Lucky {
static version: string;
protected readonly version: string;
protected readonly config: ConfigType;
protected readonly ctx: CanvasRenderingContext2D;
protected htmlFontSize: number;
protected rAF: Function;
protected boxWidth: number;
protected boxHeight: number;
protected data: {
width: string | number;
height: string | number;
};
/**
*
* @param config
*/
constructor(config: string | HTMLDivElement | UserConfigType, data: {
width: string | number;
height: string | number;
});
/**
* /
*/
protected resize(): void;
/**
*
*/
protected initLucky(): void;
/**
*
* @param e
*/
protected handleClick(e: MouseEvent): void;
/**
*
*/
protected setHTMLFontSize(): void;
clearCanvas(): void;
/**
*
* window ,
*/
protected setDpr(): void;
/**
* canvas的宽高
*/
private resetWidthAndHeight;
/**
* dpr canvas
*/
protected zoomCanvas(): void;
/**
* window
*/
private initWindowFunction;
isWeb(): boolean;
/**
*
* @param src
* @param info
*/
protected loadImg(src: string, info: ImgItemType, resolveName?: string): Promise<ImgType>;
/**
*
* @param imgObj
* @param rectInfo: [x轴位置, y轴位置, , ]
*/
protected drawImage(ctx: CanvasRenderingContext2D, imgObj: ImgType, ...rectInfo: [...Tuple<number, 4>, ...Partial<Tuple<number, 4>>]): void;
/**
*
* @param imgObj
* @param imgInfo
* @param maxWidth
* @param maxHeight
* @return [, ]
*/
protected computedWidthAndHeight(imgObj: ImgType, imgInfo: ImgItemType, maxWidth: number, maxHeight: number): [number, number];
/**
*
* @param { string } value
* @param { number } denominator
* @return { number }
*/
protected changeUnits(value: string, denominator?: number): number;
/**
*
* @param length
* @param maxLength
* @return
*/
protected getLength(length: string | number | undefined, maxLength?: number): number;
/**
* ()X坐标
* @param width
* @param col
*/
protected getOffsetX(width: number, maxWidth?: number): number;
protected getOffscreenCanvas(width: number, height: number): {
_offscreenCanvas: HTMLCanvasElement;
_ctx: CanvasRenderingContext2D;
} | void;
/**
* ()
* @param data
* @param key
* @param value
*/
$set(data: object, key: string | number, value: any): void;
/**
* ()
* @param data
* @param key
* @param callback
*/
protected $computed(data: object, key: string, callback: Function): void;
/**
* create user watcher
* @param expr
* @param handler
* @param watchOpt
* @return ()
*/
protected $watch(expr: string | Function, handler: Function | WatchOptType, watchOpt?: WatchOptType): Function;
}
declare type PrizeFontType$2 = FontItemType & FontExtendType;
declare type ButtonFontType$1 = FontItemType & {};
declare type BlockImgType$2 = ImgItemType & {
rotate?: boolean;
};
declare type PrizeImgType$2 = ImgItemType & {};
declare type ButtonImgType$1 = ImgItemType & {};
declare type BlockType$2 = {
padding?: string;
background?: BackgroundType;
imgs?: Array<BlockImgType$2>;
};
declare type PrizeType$2 = {
range?: number;
background?: BackgroundType;
fonts?: Array<PrizeFontType$2>;
imgs?: Array<PrizeImgType$2>;
};
declare type ButtonType$1 = {
radius?: string;
pointer?: boolean;
background?: BackgroundType;
fonts?: Array<ButtonFontType$1>;
imgs?: Array<ButtonImgType$1>;
};
declare type DefaultConfigType$2 = {
gutter?: string | number;
offsetDegree?: number;
speed?: number;
speedFunction?: string;
accelerationTime?: number;
decelerationTime?: number;
stopRange?: number;
};
declare type DefaultStyleType$2 = {
background?: BackgroundType;
fontColor?: PrizeFontType$2['fontColor'];
fontSize?: PrizeFontType$2['fontSize'];
fontStyle?: PrizeFontType$2['fontStyle'];
fontWeight?: PrizeFontType$2['fontWeight'];
lineHeight?: PrizeFontType$2['lineHeight'];
wordWrap?: PrizeFontType$2['wordWrap'];
lengthLimit?: PrizeFontType$2['lengthLimit'];
lineClamp?: PrizeFontType$2['lineClamp'];
};
declare type StartCallbackType$1 = (e: MouseEvent) => void;
declare type EndCallbackType$2 = (prize: object) => void;
interface LuckyWheelConfig {
width: string | number;
height: string | number;
blocks?: Array<BlockType$2>;
prizes?: Array<PrizeType$2>;
buttons?: Array<ButtonType$1>;
defaultConfig?: DefaultConfigType$2;
defaultStyle?: DefaultStyleType$2;
start?: StartCallbackType$1;
end?: EndCallbackType$2;
}
declare class LuckyWheel extends Lucky {
private blocks;
private prizes;
private buttons;
private defaultConfig;
private defaultStyle;
private _defaultConfig;
private _defaultStyle;
private startCallback?;
private endCallback?;
private Radius;
private prizeRadius;
private prizeDeg;
private prizeAng;
private rotateDeg;
private maxBtnRadius;
private startTime;
private endTime;
private stopDeg;
private endDeg;
private FPS;
/**
*
* step = 0 ,
* step = 1 ,
* step = 2 ,
* step = 3 ,
*/
private step;
/**
*
* prizeFlag = undefined , ,
* prizeFlag >= 0 , stop方法被调用,
* prizeFlag === -1 , stop方法被调用, ,
*/
private prizeFlag;
private ImageCache;
/**
*
* @param config
* @param data
*/
constructor(config: UserConfigType, data: LuckyWheelConfig);
protected resize(): void;
protected initLucky(): void;
/**
*
* @param data
*/
private initData;
/**
*
*/
private initComputed;
/**
*
*/
private initWatch;
/**
* canvas
*/
init(): Promise<void>;
private initImageCache;
/**
* canvas点击事件
* @param e
*/
protected handleClick(e: MouseEvent): void;
/**
*
* @param cellName
* @param cellIndex
* @param imgName
* @param imgIndex
*/
private loadAndCacheImg;
private drawBlock;
/**
*
*/
protected draw(): void;
/**
*
*/
private carveOnGunwaleOfAMovingBoat;
/**
* 对外暴露: 开始抽奖方法
*/
play(): void;
/**
* 对外暴露: 缓慢停止方法
* @param index
*/
stop(index?: number): void;
/**
*
* @param num
*/
private run;
/**
*
* @param x
* @param y
*/
protected conversionAxis(x: number, y: number): [number, number];
}
declare type PrizeFontType$1 = FontItemType & FontExtendType;
declare type ButtonFontType = FontItemType & FontExtendType;
declare type BlockImgType$1 = ImgItemType & {};
declare type PrizeImgType$1 = ImgItemType & {
activeSrc?: string;
};
declare type ButtonImgType = ImgItemType & {};
declare type BlockType$1 = {
borderRadius?: BorderRadiusType;
background?: BackgroundType;
padding?: string;
paddingTop?: string | number;
paddingRight?: string | number;
paddingBottom?: string | number;
paddingLeft?: string | number;
imgs?: Array<BlockImgType$1>;
};
declare type CellType<T, U> = {
x: number;
y: number;
col?: number;
row?: number;
borderRadius?: BorderRadiusType;
background?: BackgroundType;
shadow?: ShadowType;
fonts?: Array<T>;
imgs?: Array<U>;
};
declare type PrizeType$1 = CellType<PrizeFontType$1, PrizeImgType$1> & {
range?: number;
disabled?: boolean;
};
declare type ButtonType = CellType<ButtonFontType, ButtonImgType> & {
callback?: Function;
};
declare type DefaultConfigType$1 = {
gutter?: number;
speed?: number;
accelerationTime?: number;
decelerationTime?: number;
};
declare type DefaultStyleType$1 = {
borderRadius?: BorderRadiusType;
background?: BackgroundType;
shadow?: ShadowType;
fontColor?: PrizeFontType$1['fontColor'];
fontSize?: PrizeFontType$1['fontSize'];
fontStyle?: PrizeFontType$1['fontStyle'];
fontWeight?: PrizeFontType$1['fontWeight'];
lineHeight?: PrizeFontType$1['lineHeight'];
wordWrap?: PrizeFontType$1['wordWrap'];
lengthLimit?: PrizeFontType$1['lengthLimit'];
lineClamp?: PrizeFontType$1['lineClamp'];
};
declare type ActiveStyleType = {
background?: BackgroundType;
shadow?: ShadowType;
fontColor?: PrizeFontType$1['fontColor'];
fontSize?: PrizeFontType$1['fontSize'];
fontStyle?: PrizeFontType$1['fontStyle'];
fontWeight?: PrizeFontType$1['fontWeight'];
lineHeight?: PrizeFontType$1['lineHeight'];
};
declare type RowsType = number;
declare type ColsType = number;
declare type StartCallbackType = (e: MouseEvent, button?: ButtonType) => void;
declare type EndCallbackType$1 = (prize: object) => void;
interface LuckyGridConfig {
width: string | number;
height: string | number;
rows?: RowsType;
cols?: ColsType;
blocks?: Array<BlockType$1>;
prizes?: Array<PrizeType$1>;
buttons?: Array<ButtonType>;
button?: ButtonType;
defaultConfig?: DefaultConfigType$1;
defaultStyle?: DefaultStyleType$1;
activeStyle?: ActiveStyleType;
start?: StartCallbackType;
end?: EndCallbackType$1;
}
declare class LuckyGrid extends Lucky {
private rows;
private cols;
private blocks;
private prizes;
private buttons;
private button?;
private defaultConfig;
private defaultStyle;
private activeStyle;
private _defaultConfig;
private _defaultStyle;
private _activeStyle;
private startCallback?;
private endCallback?;
private cellWidth;
private cellHeight;
private startTime;
private endTime;
private currIndex;
private stopIndex;
private endIndex;
private demo;
private timer;
private FPS;
/**
*
* step = 0 ,
* step = 1 ,
* step = 2 ,
* step = 3 ,
*/
private step;
/**
*
* prizeFlag = undefined , ,
* prizeFlag >= 0 , stop方法被调用,
* prizeFlag === -1 , stop方法被调用, ,
*/
private prizeFlag;
private cells;
private prizeArea;
private ImageCache;
/**
*
* @param config
* @param data
*/
constructor(config: UserConfigType, data: LuckyGridConfig);
protected resize(): void;
protected initLucky(): void;
/**
*
* @param data
*/
private initData;
/**
*
*/
private initComputed;
/**
*
*/
private initWatch;
/**
* canvas
*/
init(): Promise<void>;
private initImageCache;
/**
* canvas点击事件
* @param e
*/
protected handleClick(e: MouseEvent): void;
/**
*
* @param cellName
* @param cellIndex
* @param imgName
* @param imgIndex
*/
private loadAndCacheImg;
/**
*
*/
protected draw(): void;
/**
*
* @param x
* @param y
* @param width
* @param height
* @param background
* @param isActive
*/
private handleBackground;
/**
*
*/
private carveOnGunwaleOfAMovingBoat;
/**
* 对外暴露: 开始抽奖方法
*/
play(): void;
/**
* 对外暴露: 缓慢停止方法
* @param index
*/
stop(index?: number): void;
/**
*
* @param num
*/
private run;
/**
*
* @param { array } [..., col, row]
* @return { array } [..., width, height]
*/
private getGeometricProperty;
/**
*
* @param x
* @param y
*/
protected conversionAxis(x: number, y: number): [number, number];
}
declare type PrizeFontType = FontItemType & FontExtendType;
declare type BlockImgType = ImgItemType & {};
declare type PrizeImgType = ImgItemType;
declare type BlockType = {
borderRadius?: BorderRadiusType;
background?: BackgroundType;
padding?: string;
paddingTop?: string | number;
paddingRight?: string | number;
paddingBottom?: string | number;
paddingLeft?: string | number;
imgs?: Array<BlockImgType>;
};
declare type PrizeType = {
borderRadius?: BorderRadiusType;
background?: BackgroundType;
fonts?: Array<PrizeFontType>;
imgs?: Array<PrizeImgType>;
};
declare type SlotType = {
order?: number[];
speed?: number;
direction?: 1 | -1;
};
declare type DefaultConfigType = {
/**
* vertical
* horizontal
*/
mode?: 'vertical' | 'horizontal';
/**
* = `vertical`
* 1 bottom to top
* -1 top to bottom
* = `horizontal`
* 1 right to left
* -1 left to right
*/
direction?: 1 | -1;
rowSpacing?: number;
colSpacing?: number;
speed?: number;
accelerationTime?: number;
decelerationTime?: number;
};
declare type DefaultStyleType = {
borderRadius?: BorderRadiusType;
background?: BackgroundType;
fontColor?: PrizeFontType['fontColor'];
fontSize?: PrizeFontType['fontSize'];
fontStyle?: PrizeFontType['fontStyle'];
fontWeight?: PrizeFontType['fontWeight'];
lineHeight?: PrizeFontType['lineHeight'];
wordWrap?: PrizeFontType['wordWrap'];
lengthLimit?: PrizeFontType['lengthLimit'];
lineClamp?: PrizeFontType['lineClamp'];
};
declare type EndCallbackType = (prize: PrizeType | undefined) => void;
interface SlotMachineConfig {
width: string | number;
height: string | number;
blocks?: Array<BlockType>;
prizes?: Array<PrizeType>;
slots?: Array<SlotType>;
defaultConfig?: DefaultConfigType;
defaultStyle?: DefaultStyleType;
end?: EndCallbackType;
}
declare class SlotMachine extends Lucky {
private blocks;
private prizes;
private slots;
private defaultConfig;
private _defaultConfig;
private defaultStyle;
private _defaultStyle;
private endCallback;
private _offscreenCanvas?;
private cellWidth;
private cellHeight;
private cellAndSpacing;
private widthAndSpacing;
private heightAndSpacing;
private FPS;
private scroll;
private stopScroll;
private endScroll;
private startTime;
private endTime;
/**
*
* step = 0 ,
* step = 1 ,
* step = 2 ,
* step = 3 ,
*/
private step;
/**
*
* prizeFlag = undefined , ,
* prizeFlag >= 0 , stop方法被调用,
* prizeFlag === -1 , stop方法被调用, ,
*/
private prizeFlag;
private prizeArea?;
private ImageCache;
/**
*
* @param config
* @param data
*/
constructor(config: UserConfigType, data: SlotMachineConfig);
protected resize(): void;
protected initLucky(): void;
/**
*
* @param data
*/
private initData;
/**
*
*/
private initComputed;
/**
*
*/
private initWatch;
/**
* canvas
*/
init(): Promise<void>;
private initImageCache;
/**
*
* @param cellName
* @param cellIndex
* @param imgName
* @param imgIndex
*/
private loadAndCacheImg;
/**
* canvas
*/
protected drawOffscreenCanvas(): void;
/**
*
*/
protected drawBlocks(): SlotMachine['prizeArea'];
/**
*
*/
protected draw(): void;
/**
*
*/
private carveOnGunwaleOfAMovingBoat;
/**
* 对外暴露: 开始抽奖方法
*/
play(): void;
stop(index: number | number[]): void;
/**
*
* @param num
*/
private run;
private displacement;
private displacementWidthOrHeight;
}
/**
*
* @param img
* @param radius
* @returns canvas
*/
declare const cutRound: (img: ImgType, radius: number) => ImgType;
/**
*
* @param img
* @param opacity
* @returns canvas
*/
declare const opacity: (img: ImgType, opacity: number) => ImgType;
export { LuckyGrid, LuckyWheel, SlotMachine, cutRound, opacity };

View File

@ -0,0 +1,498 @@
<template>
<view class="prize-wheel">
<view class="prize-container" :style="containerStyle">
<view class="prize-strip" :style="stripStyle">
<!-- 左侧复制项 -->
<view
class="prize-item"
v-for="(item, index) in visibleItems.prefix"
:key="`prefix-${index}`"
:style="itemStyle"
>
<slot :item="item">
<view class="default-prize" :style="{ color: item.color }">{{item.value}}</view>
</slot>
</view>
<!-- 主要项 -->
<view
class="prize-item"
v-for="(item, index) in visibleItems.main"
:key="`main-${index}`"
:class="{'prize-active': isActive && activeIndex === index}"
:style="itemStyle"
>
<slot :item="item">
<view class="default-prize" :style="{ color: item.color }">{{item.value}}</view>
</slot>
</view>
<!-- 右侧复制项 -->
<view
class="prize-item"
v-for="(item, index) in visibleItems.suffix"
:key="`suffix-${index}`"
:style="itemStyle"
>
<slot :item="item">
<view class="default-prize" :style="{ color: item.color }">{{item.value}}</view>
</slot>
</view>
</view>
</view>
<!-- 中心指示器 -->
<view class="center-indicator"></view>
</view>
</template>
<script>
export default {
name: 'PrizeWheel',
props: {
//
prizes: {
type: Array,
default: () => []
},
// ()
duration: {
type: Number,
default: 4
},
// ()
bufferCount: {
type: Number,
default: 5
},
//
itemWidth: {
type: Number,
default: () => uni.upx2px(180)
},
//
itemHeight: {
type: Number,
default: () => uni.upx2px(150)
},
//
highlight: {
type: Boolean,
default: true
},
//
maxSpeed: {
type: Number,
default: 2000
}
},
data() {
return {
isSpinning: false, //
isActive: false, //
activeIndex: -1, //
currentOffset: 0, //
targetPrize: null, //
targetIndex: -1, //
speedPhase: 'initial', // initial, accelerating, constant, decelerating, stopping
animationId: null, // ID
lastTimestamp: 0, //
speed: 0, // (px/s)
totalSpinCount: 0, //
itemsCount: 0, //
autoResetTimer: null, //
}
},
computed: {
// (++)
visibleItems() {
if (!this.prizes || this.prizes.length === 0) {
return { prefix: [], main: [], suffix: [] }
}
//
const baseItems = [...this.prizes]
this.itemsCount = baseItems.length
//
let mainItems = []
const repeatCount = Math.ceil(30 / baseItems.length) //
for (let i = 0; i < repeatCount; i++) {
mainItems = [...mainItems, ...baseItems]
}
// ()
const prefix = [...baseItems]
const suffix = [...baseItems]
return {
prefix,
main: mainItems,
suffix
}
},
//
containerStyle() {
return {
height: `${this.itemHeight}px`,
width: '100%',
overflow: 'hidden'
}
},
//
stripStyle() {
let transitionStyle = this.isSpinning ? 'none' : `transform ${this.duration / 3}s cubic-bezier(0.34, 1.56, 0.64, 1)`
return {
transform: `translateX(${this.currentOffset}px)`,
transition: transitionStyle
}
},
//
itemStyle() {
return {
width: `${this.itemWidth}px`,
height: `${this.itemHeight}px`,
flexShrink: 0
}
},
//
centerOffset() {
const containerWidth = uni.getSystemInfoSync().windowWidth
return containerWidth / 2 - this.itemWidth / 2
},
// ()
cycleWidth() {
return this.itemWidth * this.prizes.length
}
},
mounted() {
//
this.resetPosition()
},
beforeDestroy() {
//
this.stopAnimation()
if (this.autoResetTimer) {
clearTimeout(this.autoResetTimer)
}
},
methods: {
//
resetPosition() {
//
this.currentOffset = this.centerOffset - (this.prizes.length * this.itemWidth)
},
//
startSpin() {
if (this.isSpinning) return
this.isSpinning = true
this.speedPhase = 'accelerating'
this.speed = 30 //
this.targetPrize = null
this.targetIndex = -1
this.totalSpinCount = 0
this.lastTimestamp = performance.now()
//
this.$emit('spin-start')
//
this.animationId = requestAnimationFrame(this.animate)
},
//
setPrize(prize) {
if (!this.isSpinning) return
//
this.targetPrize = prize
//
this.targetIndex = this.prizes.findIndex(item =>
(item.id && item.id === prize.id) ||
(item.value && item.value === prize.value)
)
if (this.targetIndex === -1) {
console.warn('目标奖品不在奖品列表中')
// 使
this.targetIndex = 0
}
//
setTimeout(() => {
if (this.isSpinning) {
this.speedPhase = 'decelerating'
}
}, 1000) //
},
//
animate(timestamp) {
if (!this.isSpinning) return
//
const delta = timestamp - this.lastTimestamp
this.lastTimestamp = timestamp
//
switch(this.speedPhase) {
case 'accelerating':
//
this.speed = Math.min(this.maxSpeed, this.speed * 1.08)
if (this.speed >= this.maxSpeed) {
this.speedPhase = 'constant'
}
break
case 'decelerating':
//
this.speed *= 0.97
//
if (this.speed < 200 && this.targetIndex !== -1) {
this.speedPhase = 'stopping'
this.prepareToStop()
}
break
case 'stopping':
// -
this.speed *= 0.85
//
if (this.speed < 10) {
this.finalizePosition()
this.stopAnimation()
return
}
break
}
//
this.moveStrip(delta)
//
this.animationId = requestAnimationFrame(this.animate)
},
//
moveStrip(deltaTime) {
//
const distance = (this.speed * deltaTime) / 1000
//
this.currentOffset -= distance
//
this.checkResetPosition()
//
this.updateActiveItem()
},
//
checkResetPosition() {
const cycleWidth = this.itemWidth * this.prizes.length
//
if (Math.abs(this.currentOffset) > cycleWidth * 3) {
//
const remainder = this.currentOffset % cycleWidth
//
this.currentOffset = this.centerOffset - cycleWidth + remainder
this.totalSpinCount++
}
},
//
updateActiveItem() {
if (!this.highlight) return
//
const relativePos = -this.currentOffset + this.centerOffset
//
const cyclePos = (relativePos % this.cycleWidth + this.cycleWidth) % this.cycleWidth
//
const itemIndex = Math.floor(cyclePos / this.itemWidth)
if (itemIndex >= 0 && itemIndex < this.prizes.length) {
this.isActive = true
this.activeIndex = itemIndex
} else {
this.isActive = false
}
},
//
prepareToStop() {
if (this.targetIndex === -1) return
//
const relativePos = -this.currentOffset + this.centerOffset
//
const cyclePos = (relativePos % this.cycleWidth + this.cycleWidth) % this.cycleWidth
//
const currentItemIndex = Math.floor(cyclePos / this.itemWidth)
//
//
// -
const decelerationFactor = 0.95
//
//
let stepsToTarget = this.targetIndex - currentItemIndex
if (stepsToTarget <= 0) {
stepsToTarget += this.prizes.length
}
//
// 线
if (this.speed > 100) {
this.speed = Math.max(100, this.speed * decelerationFactor)
}
},
//
finalizePosition() {
if (this.targetIndex === -1) return
// 使
// 使
const relativePos = -this.currentOffset + this.centerOffset
const cyclePos = (relativePos % this.cycleWidth + this.cycleWidth) % this.cycleWidth
const currentItemIndex = Math.floor(cyclePos / this.itemWidth)
//
let adjustedOffset = this.currentOffset
//
if (Math.abs(currentItemIndex - this.targetIndex) <= this.prizes.length / 2) {
const delta = (currentItemIndex - this.targetIndex) * this.itemWidth
adjustedOffset += delta
}
// 使CSS
this.isSpinning = false
this.currentOffset = adjustedOffset
//
this.isActive = true
this.activeIndex = this.targetIndex
},
//
stopAnimation() {
if (!this.isSpinning && !this.animationId) return
cancelAnimationFrame(this.animationId)
this.animationId = null
this.isSpinning = false
//
this.$emit('spin-end', this.targetPrize || this.prizes[this.activeIndex])
//
if (this.autoResetTimer) {
clearTimeout(this.autoResetTimer)
this.autoResetTimer = null
}
}
}
}
</script>
<style lang="scss">
.prize-wheel {
position: relative;
width: 100%;
.prize-container {
position: relative;
overflow: hidden;
}
.prize-strip {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
}
.prize-item {
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
transition: transform 0.2s ease;
&.prize-active {
transform: scale(1.1);
z-index: 1;
.default-prize {
background-color: rgba(255, 215, 0, 0.3);
box-shadow: 0 0 10px rgba(255, 215, 0, 0.5);
}
}
}
.default-prize {
width: 90%;
height: 90%;
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(255, 255, 255, 0.8);
border-radius: 8px;
font-size: 30rpx;
padding: 10rpx;
transition: all 0.2s ease;
}
.center-indicator {
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 2px;
height: 100%;
background-color: #ff5a5f;
z-index: 10;
&:before {
content: '';
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 0;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-top: 10px solid #ff5a5f;
}
&:after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 0;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-bottom: 10px solid #ff5a5f;
}
}
}
</style>

14
package-lock.json generated
View File

@ -5,15 +5,29 @@
"packages": {
"": {
"dependencies": {
"@lucky-canvas/uni": "^0.0.14",
"js-md5": "^0.8.3",
"uqrcodejs": "^4.0.7"
}
},
"node_modules/@lucky-canvas/uni": {
"version": "0.0.14",
"resolved": "https://registry.npmjs.org/@lucky-canvas/uni/-/uni-0.0.14.tgz",
"integrity": "sha512-QSPMrYj5gxVxehHc9XwpL9vVoSLJd7RS70sHGO12QNHJ7t6hztgXdecMRel9/dvLlo9fRwOBdnthF332qgWeLA==",
"dependencies": {
"lucky-canvas": "~1.7.19"
}
},
"node_modules/js-md5": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.8.3.tgz",
"integrity": "sha512-qR0HB5uP6wCuRMrWPTrkMaev7MJZwJuuw4fnwAzRgP4J4/F8RwtodOKpGp4XpqsLBFzzgqIO42efFAyz2Et6KQ=="
},
"node_modules/lucky-canvas": {
"version": "1.7.27",
"resolved": "https://registry.npmjs.org/lucky-canvas/-/lucky-canvas-1.7.27.tgz",
"integrity": "sha512-Ftz6qD+863bI7xijBmZg3dw3cNEc7odPr70EZQcGA14y3TgTAzH65HPosOCd6kKUlMwhntBaHMx3onoj9MtJRQ=="
},
"node_modules/uqrcodejs": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/uqrcodejs/-/uqrcodejs-4.0.7.tgz",

View File

@ -1,5 +1,6 @@
{
"dependencies": {
"@lucky-canvas/uni": "^0.0.14",
"js-md5": "^0.8.3",
"uqrcodejs": "^4.0.7"
}

View File

@ -389,6 +389,18 @@
"style": {
"navigationStyle": "custom"
}
},
{
"path" : "pages/shouye/prize-wheel-demo",
"style": {
"navigationStyle": "custom"
}
},
{
"path" : "pages/shouye/canvas-prize-demo",
"style": {
"navigationStyle": "custom"
}
}
],
"subPackages": [{

View File

@ -0,0 +1,223 @@
<template>
<page-container title="Canvas抽奖特效演示" :showBack="true">
<view class="demo-container">
<view class="prize-wheel-wrapper">
<canvas-prize-wheel
ref="canvasPrizeWheel"
:prizes="prizes"
:duration="4"
:itemWidth="itemWidth"
:itemHeight="itemHeight"
:backgroundColor="'rgba(255, 255, 255, 0.9)'"
:highlightColor="'rgba(255, 215, 0, 0.5)'"
:highlightShadow="'rgba(255, 215, 0, 0.8)'"
:borderRadius="8"
@spin-start="onSpinStart"
@spin-end="onSpinEnd">
</canvas-prize-wheel>
</view>
<view class="control-panel">
<button class="start-btn" @click="startLottery" :disabled="isSpinning">开始抽奖</button>
<view class="result-display" v-if="prizeResult">
<text class="result-label">抽奖结果:</text>
<text class="result-value" :style="{ color: prizeResult.color }">{{prizeResult.value}}</text>
</view>
</view>
</view>
</page-container>
</template>
<script>
import CanvasPrizeWheel from '@/components/canvas-prize-wheel/canvas-prize-wheel.vue'
export default {
components: {
CanvasPrizeWheel
},
data() {
return {
// - 使
prizes: [
{ id: 1, value: '一等奖', color: '#ff0000', bgColor: 'rgba(255,0,0,0.1)' },
{ id: 2, value: '二等奖', color: '#00ff00', bgColor: 'rgba(0,255,0,0.1)' },
{ id: 3, value: '三等奖', color: '#0000ff', bgColor: 'rgba(0,0,255,0.1)' },
{ id: 4, value: '四等奖', color: '#ffff00', bgColor: 'rgba(255,255,0,0.1)' },
{ id: 5, value: '五等奖', color: '#ff00ff', bgColor: 'rgba(255,0,255,0.1)' },
{ id: 6, value: '六等奖', color: '#00ffff', bgColor: 'rgba(0,255,255,0.1)' },
{ id: 7, value: '谢谢', color: '#ff8800', bgColor: 'rgba(255,136,0,0.1)' }
],
isSpinning: false,
prizeResult: null,
itemWidth: uni.upx2px(150),
itemHeight: uni.upx2px(120)
}
},
mounted() {
// Canvas
this.$nextTick(() => {
setTimeout(() => {
if (this.$refs.canvasPrizeWheel) {
this.$refs.canvasPrizeWheel.render()
}
}, 500)
})
},
methods: {
//
startLottery() {
if (this.isSpinning) return
this.isSpinning = true
this.prizeResult = null
//
this.$refs.canvasPrizeWheel.startSpin()
//
// API
setTimeout(() => {
//
const randomIndex = Math.floor(Math.random() * this.prizes.length)
const result = this.prizes[randomIndex]
//
this.$refs.canvasPrizeWheel.setPrize(result)
}, 2000) // 2
},
//
onSpinStart() {
console.log('抽奖开始')
//
uni.showToast({
title: '抽奖开始',
icon: 'none'
})
},
//
onSpinEnd(prize) {
console.log('抽奖结束', prize)
this.isSpinning = false
this.prizeResult = prize
//
uni.showToast({
title: `恭喜获得: ${prize.value}`,
icon: 'success',
duration: 2000
})
//
// this.saveAndShareResult()
},
//
async saveAndShareResult() {
try {
// Canvas
const imagePath = await this.$refs.canvasPrizeWheel.exportImage()
//
uni.saveImageToPhotosAlbum({
filePath: imagePath,
success: () => {
uni.showToast({
title: '已保存到相册',
icon: 'success'
})
},
fail: (err) => {
console.error('保存图片失败', err)
}
})
} catch (error) {
console.error('导出图片失败', error)
}
}
}
}
</script>
<style lang="scss">
.demo-container {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-sizing: border-box;
background-image: url($imgurl + 'common/slot_bg.webp');
background-size: cover;
background-position: center;
.prize-wheel-wrapper {
width: 100%;
padding: 0;
height: 120rpx;
background-image: url($imgurl + 'common/slot1.png');
background-size: cover;
background-position: center;
position: relative;
margin: 40rpx 0;
display: flex;
align-items: center;
justify-content: center;
}
.control-panel {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
padding: 20rpx;
.start-btn {
width: 300rpx;
height: 90rpx;
background: linear-gradient(to right, #ff5a5f, #ff8a5f);
color: white;
border-radius: 45rpx;
display: flex;
justify-content: center;
align-items: center;
font-size: 32rpx;
margin-bottom: 30rpx;
box-shadow: 0 4rpx 10rpx rgba(255, 90, 95, 0.3);
&:active {
transform: scale(0.98);
}
&[disabled] {
background: #cccccc;
color: #888888;
}
}
.result-display {
margin-top: 20rpx;
background-color: rgba(255, 255, 255, 0.8);
padding: 20rpx 40rpx;
border-radius: 10rpx;
display: flex;
flex-direction: column;
align-items: center;
.result-label {
font-size: 28rpx;
color: #666;
margin-bottom: 10rpx;
}
.result-value {
font-size: 40rpx;
font-weight: bold;
}
}
}
}
</style>

View File

@ -8,7 +8,7 @@
<view class="navLeft align-center" :style="{ top: $sys().statusBarHeight + 'px' }" @tap="$c.back(1)">
<view class="flex" style="width: 100%">
<view class="title1" style="width: 166rpx; height: 64rpx; margin-top: 5rpx;" >
<image :src="$img1('common/home_logo.png')" style="width: 166rpx; height: 64rpx;" mode=""></image>
<image :src="$img1('common/home_logo.png')" style="width: 166rpx; height: 64rpx;" mode="" @click.stop="jumapSlots()"></image>
</view>
</view>
</view>

View File

@ -0,0 +1,202 @@
<template>
<page-container title="抽奖特效演示" :showBack="true">
<view class="demo-container">
<view class="prize-wheel-wrapper">
<prize-wheel
ref="prizeWheel"
:prizes="prizes"
:duration="4"
:bufferCount="3"
:itemWidth="itemWidth"
:itemHeight="itemHeight"
@spin-start="onSpinStart"
@spin-end="onSpinEnd">
<!-- 自定义奖品插槽可选 -->
<template v-slot="{item}">
<view class="custom-prize" :style="{ backgroundColor: item.bgColor }">
<text :style="{ color: item.color }">{{item.value}}</text>
</view>
</template>
</prize-wheel>
</view>
<view class="control-panel">
<button class="start-btn" @click="startLottery" :disabled="isSpinning">开始抽奖</button>
<view class="result-display" v-if="prizeResult">
<text class="result-label">抽奖结果:</text>
<text class="result-value" :style="{ color: prizeResult.color }">{{prizeResult.value}}</text>
</view>
</view>
</view>
</page-container>
</template>
<script>
import PrizeWheel from '@/components/prize-wheel/prize-wheel.vue'
export default {
components: {
PrizeWheel
},
data() {
return {
//
prizes: [
{ id: 1, value: '一等奖', color: '#ff0000', bgColor: 'rgba(255,0,0,0.1)' },
{ id: 2, value: '二等奖', color: '#00ff00', bgColor: 'rgba(0,255,0,0.1)' },
{ id: 3, value: '三等奖', color: '#0000ff', bgColor: 'rgba(0,0,255,0.1)' },
{ id: 4, value: '四等奖', color: '#ffff00', bgColor: 'rgba(255,255,0,0.1)' },
{ id: 5, value: '五等奖', color: '#ff00ff', bgColor: 'rgba(255,0,255,0.1)' },
{ id: 6, value: '六等奖', color: '#00ffff', bgColor: 'rgba(0,255,255,0.1)' },
{ id: 7, value: '七等奖', color: '#ff8800', bgColor: 'rgba(255,136,0,0.1)' },
{ id: 8, value: '八等奖', color: '#888888', bgColor: 'rgba(136,136,136,0.1)' },
{ id: 9, value: '谢谢参与', color: '#333333', bgColor: 'rgba(51,51,51,0.1)' }
],
isSpinning: false,
prizeResult: null,
itemWidth: uni.upx2px(170),
itemHeight: uni.upx2px(150)
}
},
methods: {
//
startLottery() {
if (this.isSpinning) return
this.isSpinning = true
this.prizeResult = null
//
this.$refs.prizeWheel.startSpin()
//
// API
setTimeout(() => {
// 使
const randomIndex = Math.floor(Math.random() * this.prizes.length)
const result = this.prizes[randomIndex]
//
this.$refs.prizeWheel.setPrize(result)
}, 2000) // 2
},
//
onSpinStart() {
console.log('抽奖开始')
//
uni.showToast({
title: '抽奖开始',
icon: 'none'
})
},
//
onSpinEnd(prize) {
console.log('抽奖结束', prize)
this.isSpinning = false
this.prizeResult = prize
//
uni.showToast({
title: `恭喜获得: ${prize.value}`,
icon: 'success',
duration: 2000
})
}
}
}
</script>
<style lang="scss">
.demo-container {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-sizing: border-box;
background-image: url($imgurl + 'common/slot_bg.webp');
background-size: cover;
background-position: center;
.prize-wheel-wrapper {
width: 100%;
padding: 40rpx 0;
background-image: url($imgurl + 'common/slot1.png');
background-size: cover;
background-position: center;
position: relative;
margin-bottom: 40rpx;
}
.control-panel {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
padding: 20rpx;
.start-btn {
width: 300rpx;
height: 90rpx;
background: linear-gradient(to right, #ff5a5f, #ff8a5f);
color: white;
border-radius: 45rpx;
display: flex;
justify-content: center;
align-items: center;
font-size: 32rpx;
margin-bottom: 30rpx;
box-shadow: 0 4rpx 10rpx rgba(255, 90, 95, 0.3);
&:active {
transform: scale(0.98);
}
&[disabled] {
background: #cccccc;
color: #888888;
}
}
.result-display {
margin-top: 20rpx;
background-color: rgba(255, 255, 255, 0.8);
padding: 20rpx 40rpx;
border-radius: 10rpx;
display: flex;
flex-direction: column;
align-items: center;
.result-label {
font-size: 28rpx;
color: #666;
margin-bottom: 10rpx;
}
.result-value {
font-size: 40rpx;
font-weight: bold;
}
}
}
.custom-prize {
width: 90%;
height: 90%;
display: flex;
justify-content: center;
align-items: center;
border-radius: 8px;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.1);
text {
font-weight: bold;
font-size: 30rpx;
}
}
}
</style>

View File

@ -1,175 +1,312 @@
<template>
<page-container title="抽奖特效" :showBack="true">
<page-container title="抽奖特效" :showBack="true">
<view class="content-container">
<view>
<view class="slot-view">
<!-- 老虎机组件 -->
<SlotMachine
ref="myLucky"
:width="windowWidth"
height="800rpx"
:blocks="blocks"
:slots="slots"
:prizes="prizes"
:defaultConfig="defaultConfig"
@start="startCallBack"
@end="endCallBack"
>
</SlotMachine>
</view>
</view>
</view>
<view class="content-container">
<view class="slot-view">
<view class=""
style="width: 100%; height: 152.78rpx; background-color: aquamarine; margin-top: 195rpx;">
<tiner-swiper-loop ref="lottryRef" :itemW="lotteryItemSize[0]" :itemH="lotteryItemSize[1]"
:items="items" :aTime="5" :isLottry="true" :excessCount="0" :disableTouch="true"
style="height: 152.78rpx;">
<template v-slot="{item,index}">
<view class="item-lottry" :style="{ color: item.color }">{{item.value}}</view>
</template>
</tiner-swiper-loop>
</view>
</view>
</view>
<button style="margin-top: 20rpx;" @click="onStartDraw">开始抽奖</button>
</page-container>
<!-- 开始抽奖按钮 -->
<button
style="
margin-top: 20rpx;
position: absolute;
bottom: 20rpx;
left: 50%;
transform: translateX(-50%);
"
@click="onStartDraw"
>
开始抽奖
</button>
</page-container>
</template>
<script>
export default {
data() {
return {
//
items: [{
value: '一等奖',
color: '#ff0000'
},
{
value: '七等奖',
color: '#ff8800'
}, {
value: '七等奖',
color: '#ff8800'
}, {
value: '七等奖',
color: '#ff8800'
},
{
value: '二等奖',
color: '#00ff00'
},
{
value: '七等奖',
color: '#ff8800'
}, {
value: '七等奖',
color: '#ff8800'
},
{
value: '七等奖',
color: '#ff8800'
},
{
value: '三等奖',
color: '#0000ff'
},
{
value: '七等奖',
color: '#ff8800'
},
{
value: '四等奖',
color: '#ffff00'
},
{
value: '七等奖',
color: '#ff8800'
}, {
value: '七等奖',
color: '#ff8800'
}, {
value: '七等奖',
color: '#ff8800'
},
{
value: '五等奖',
color: '#ff00ff'
},
{
value: '六等奖',
color: '#00ffff'
},
{
value: '七等奖',
color: '#ff8800'
},
{
value: '七等奖',
color: '#ff8800'
},
],
//
import SlotMachine from "@/components/@lucky-canvas/uni/slot-machine";
function shuffle(array) {
for (let i = array.length - 1; i > 0; i--) {
// 0 i
const j = Math.floor(Math.random() * (i + 1));
// array[i] array[j]
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
// import SlotMachine from '@lucky-canvas/uni/slot-machine' //
export default {
components: { SlotMachine },
data() {
let windowWidth = uni.getSystemInfoSync().windowWidth;
console.log(windowWidth);
let t = [
{
id: 1128,
title: "兹琪露娜提亚斯",
imgurl:
"https://image.zfunbox.cn/topic/20250515/2986e27e673ef675e02771cdebd9b822.jpg",
price: "350.00",
real_pro: "0.02000",
goods_type: 1,
doubling: 1,
is_lingzhu: 0,
},
{
id: 1129,
title: "月岗恋钟",
imgurl:
"https://image.zfunbox.cn/topic/20250515/2c5ed2097716db6bef01da718bc3c091.jpg",
price: "132.00",
real_pro: "0.02000",
goods_type: 1,
doubling: 3,
is_lingzhu: 0,
},
{
id: 1130,
title: "BANDAI万代拼装模型 1/100 MG 机动战士高达 倒A 逆A-15岁以上",
imgurl:
"https://image.zfunbox.cn/topic/20250515/e35da49b4976f156f2f98dec002274a5.png",
price: "305.00",
real_pro: "0.03000",
goods_type: 1,
doubling: 1,
is_lingzhu: 0,
},
{
id: 1131,
title: "BANDAI 万代拼装模型 MG 主天使-15岁以上",
imgurl:
"https://image.zfunbox.cn/topic/20250515/77302c6f1ea9ea6a8516cc1208174aee.png",
price: "289.00",
real_pro: "0.03000",
goods_type: 1,
doubling: 1,
is_lingzhu: 0,
},
{
id: 1132,
title: "BANDAI万代 HG00 09 1/144 座天使高达一型-15岁以上",
imgurl:
"https://image.zfunbox.cn/topic/20250515/55e816c93b5e4103a30682c586816b11.jpg",
price: "114.00",
real_pro: "0.50000",
goods_type: 1,
doubling: 3,
is_lingzhu: 0,
},
{
id: 1133,
title: "BANDAI万代拼装模型 HGUC 130 机动战士高达 杰斯塔-15岁以上",
imgurl:
"https://image.zfunbox.cn/topic/20250515/aeb6bfb8b4aa8a29796b242e4f5d56d9.png",
price: "113.00",
real_pro: "1.50000",
goods_type: 1,
doubling: 1,
is_lingzhu: 0,
},
{
id: 1134,
title: "BANDAI万代拼装模型HG26 1/144 凯列班高达 异灵高达-15岁以上",
imgurl:
"https://image.zfunbox.cn/topic/20250515/329e3a7e21772a63cea03d31f948345d.png",
price: "112.00",
real_pro: "1.00000",
goods_type: 1,
doubling: 2,
is_lingzhu: 0,
},
{
id: 1135,
title: "梦幻",
imgurl:
"https://image.zfunbox.cn/topic/20250515/d2c7e48515d393084000595074209042.jpg",
price: "41.00",
real_pro: "2.50000",
goods_type: 1,
doubling: 2,
is_lingzhu: 0,
},
{
id: 1136,
title: "谜拟丘",
imgurl:
"https://image.zfunbox.cn/topic/20250515/6031827bc455cbf86ff778d74ddffbd3.jpg",
price: "38.00",
real_pro: "1.50000",
goods_type: 1,
doubling: 1,
is_lingzhu: 0,
},
{
id: 1137,
title: "小提琴模型1个",
imgurl:
"https://image.zfunbox.cn/topic/20250515/22846dea5a933ab314998afc51abb7bb.jpg",
price: "13.80",
real_pro: "92.90000",
goods_type: 1,
doubling: 1,
is_lingzhu: 0,
},
];
let prizes = [];
for (let i = 0; i < t.length; i++) {
prizes.push({
imgs: [
{
src: t[i].imgurl,
width: "152.78rpx",
height: "152.78rpx",
},
],
background: "#ffffff",
});
}
const arr = Array.from({ length: t.length }, (_, i) => i);
let slots = [
{ order:shuffle([...arr]), speed: 20 }, //
{ order:shuffle([...arr]), speed: 20 }, //
{ order:shuffle([...arr]), speed: 20 }, //
{ order:shuffle([...arr]), speed: 20 }, //
{ order:shuffle([...arr]), speed: 20 }, //
]
console.log(slots);
return {
windowWidth: windowWidth + "px",
//
blocks: [
// { padding: "100px 0px 0px 0px", imgs:[{src:"https://image.zfunbox.cn/di.png",width:"100%",height:"100%"}]},
//https://image.zfunbox.cn/di.png
// {background:"#238E71"},
// { padding: "0px", background: "transparent" }, //
// { padding: "0px", background: "transparent" }, //
],
//
slots: slots,
//
prizes: prizes,
//
defaultConfig: {
mode: "horizontal", //
rowSpacing: "10px", //
colSpacing: "10px", //
accelerationTime:1500
},
};
},
computed: {
//
lotteryItemSize() {
let height = uni.upx2px(220);
let width = uni.upx2px(170);
return [width, height];
},
},
methods: {
//
startCallBack() {
console.log("开始抽奖");
},
//
endCallBack(prize) {
//
console.log("抽奖结束", prize);
},
//
onStartDraw() {
let windowWidth = uni.getSystemInfoSync().windowWidth;
console.log(windowWidth);
}
},
computed: {
lotteryItemSize() {
let height = uni.upx2px(220);
let width = uni.upx2px(170);
return [width, height];
}
},
methods: {
onStartDraw() {
//this.$refs.lottryRef.initLottryData();
this.bgmCtx.slotBgm.play()
this.$refs.lottryRef.startDraw();
}
}
}
//
this.$refs.myLucky.play();
this.bgmCtx.slotBgm.play()
// 使
setTimeout(() => {
//
const index = Math.floor(Math.random() * this.prizes.length);
// stop
this.$refs.myLucky.stop([0, 1, 2,3,4]);
}, 2000); // 3
},
},
};
</script>
<style lang="scss">
.content-container {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-sizing: border-box;
background-image: url($imgurl + 'common/slot_bg.webp');
background-size: cover;
background-position: center;
}
//
.content-container {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-sizing: border-box;
background-image: url($imgurl + "common/slot_bg.webp");
background-size: cover;
background-position: center;
}
.slot-title{
background: url('https://image.zfunbox.cn/di.png') no-repeat;
background-size: 100% 100%;
width: 100vw;
height: 100rpx;
}
//
.slot-view {
// background: linear-gradient(to right, #0c1b21, #218c78, #0c1b21);
background: url('https://image.zfunbox.cn/di.png') no-repeat;
background-size: 100% 100%;
// background-size: cover;
// background-position: center;
width: 100vw;
padding-top: 200rpx;
padding-bottom: 80rpx;
// height: 520rpx;
display: flex;
justify-content: center;
align-items: center;
// padding: 0rpx;
}
.slot-view {
background-image: url($imgurl + 'common/slot1.png');
background-size: cover;
background-position: center;
width: 100%;
height: 427.78rpx;
}
// 线
.view-center-line {
position: absolute;
width: 1rpx;
background-color: red;
height: 300rpx;
z-index: 10;
}
.view-center-line {
position: absolute;
width: 1rpx;
background-color: red;
height: 300rpx;
z-index: 10;
}
.item-lottry {
height: 152.78rpx;
width: 152.78rpx;
display: flex;
justify-content: center;
align-items: center;
font-size: 30rpx;
border-radius: 16rpx;
color: black;
box-sizing: border-box;
background-color: rgba(255, 255, 255, 0.8);
}
//
.item-lottry {
height: 152.78rpx;
width: 152.78rpx;
display: flex;
justify-content: center;
align-items: center;
font-size: 30rpx;
border-radius: 16rpx;
color: black;
box-sizing: border-box;
background-color: rgba(255, 255, 255, 1);
}
</style>