1、引言
在移动应用开发领域,彩票类应用因其娱乐性和实用性而备受用户青睐。本文将深入分析一个基于uni-app框架开发的超级大乐透号码生成器,从产品设计、技术架构到具体实现,全面解析这个项目的亮点与价值。
## 更多学习资料:https://www.processon.com/mindmap/60504b5ff346fb348a93b4fa
先看效果图
2、项目概述
这是一个功能完整的超级大乐透号码模拟生成应用,具备号码生成、历史记录、数据分析、一键复制等核心功能。项目采用Vue.js + uni-app技术栈,支持多端部署,体现了现代前端开发的最佳实践。
3、技术架构分析
1. 技术选型的智慧
uni-app框架的选择
- 一套代码,多端运行(H5、小程序、APP)
- 基于Vue.js,开发体验优秀
- 丰富的API支持,满足跨平台需求
Vue.js组件化设计
export default {
data() {
return {
currentNumbers: {
front: [], // 前区5个号码
back: [] // 后区2个号码
},
historyNumbers: [],
periodCounter: 1
}
}
}
2. 数据结构设计的巧思
项目采用了清晰的数据结构设计:
currentNumbers
: 当前生成的号码对象historyNumbers
: 历史记录数组periodCounter
: 期数计数器
这种设计既保证了数据的完整性,又便于后续的扩展和维护。
4、核心功能实现解析
1. 智能号码生成算法
generateNumbers() {
// 生成前区号码(1-35选5个)
const frontNumbers = []
while (frontNumbers.length < 5) {
const num = Math.floor(Math.random() * 35) + 1
if (!frontNumbers.includes(num)) {
frontNumbers.push(num)
}
}
frontNumbers.sort((a, b) => a - b)
// 生成后区号码(1-12选2个)
const backNumbers = []
while (backNumbers.length < 2) {
const num = Math.floor(Math.random() * 12) + 1
if (!backNumbers.includes(num)) {
backNumbers.push(num)
}
}
backNumbers.sort((a, b) => a - b)
}
算法亮点:
- 使用while循环确保不重复
- 自动排序,提升用户体验
- 严格按照大乐透规则实现
2. 跨平台复制功能
copyNumbers() {
const frontStr = this.currentNumbers.front.map(num => num.toString().padStart(2, '0')).join(' ')
const backStr = this.currentNumbers.back.map(num => num.toString().padStart(2, '0')).join(' ')
const copyText = `前区:${frontStr} 后区:${backStr}`
// #ifdef H5
if (navigator.clipboard) {
navigator.clipboard.writeText(copyText)
}
// #endif
// #ifdef APP-PLUS || MP-WEIXIN || MP-ALIPAY
uni.setClipboardData({
data: copyText,
success: () => {
uni.showToast({ title: '号码已复制', icon: 'success' })
}
})
// #endif
}
技术特色:
- 条件编译实现跨平台兼容
- 格式化输出,用户友好
- 完善的错误处理机制
3. 本地存储与数据持久化
saveHistory() {
try {
uni.setStorageSync('lottery_history', this.historyNumbers)
uni.setStorageSync('lottery_counter', this.periodCounter)
} catch (e) {
console.log('保存历史记录失败:', e)
}
}
loadHistory() {
try {
const history = uni.getStorageSync('lottery_history')
const counter = uni.getStorageSync('lottery_counter')
if (history) this.historyNumbers = history
if (counter) this.periodCounter = counter
} catch (e) {
console.log('加载历史记录失败:', e)
}
}
设计优势:
- 异常处理保证应用稳定性
- 数据分离存储,便于管理
- 自动加载,无缝用户体验
5、UI/UX设计亮点
1. 现代化的视觉设计
.lottery-page {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
flex-direction: column;
}
.number-ball {
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%);
border-radius: 32rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
}
设计特色:
- 渐变色彩搭配,视觉层次丰富
- 圆角设计,符合现代审美
- 阴影效果,增强立体感
2. 响应式布局设计
@media (max-width: 750rpx) {
.content-scroll {
padding: 0 16rpx 16rpx;
}
.analysis-grid {
grid-template-columns: 1fr;
gap: 16rpx;
}
}
适配策略:
- 媒体查询实现响应式
- 网格布局自适应调整
- 间距动态优化
6、完整代码
<template>
<view class="lottery-page">
<!-- 头部标题 -->
<view class="header">
<text class="title">🎱 超级大乐透</text>
<text class="subtitle">号码模拟与历史记录</text>
</view>
<scroll-view class="content-scroll" scroll-y="true">
<!-- 当前号码生成卡片 -->
<view class="card-container">
<view class="card-header">
<view class="icon-wrapper generate-icon">
<text class="icon">🎲</text>
</view>
<view class="header-content">
<text class="card-title">号码生成</text>
<text class="card-subtitle">点击生成随机中奖号码</text>
</view>
</view>
<view class="card-body">
<!-- 当前生成的号码显示 -->
<view class="current-numbers" v-if="currentNumbers.front.length > 0">
<view class="numbers-header">
<text class="numbers-title">当前号码</text>
<button class="copy-btn" @click="copyNumbers">
<text class="copy-icon">📋</text>
<text class="copy-text">复制</text>
</button>
</view>
<view class="numbers-section">
<text class="section-label">前区号码</text>
<view class="numbers-row">
<view
v-for="(num, index) in currentNumbers.front"
:key="index"
class="number-ball front-ball"
>
<text class="number-text">{{ num.toString().padStart(2, '0') }}</text>
</view>
</view>
</view>
<view class="numbers-section">
<text class="section-label">后区号码</text>
<view class="numbers-row">
<view
v-for="(num, index) in currentNumbers.back"
:key="index"
class="number-ball back-ball"
>
<text class="number-text">{{ num.toString().padStart(2, '0') }}</text>
</view>
</view>
</view>
</view>
<!-- 生成按钮 -->
<view class="generate-actions">
<button class="generate-btn primary" @click="generateNumbers">
<text class="btn-icon">🎯</text>
<text class="btn-text">生成号码</text>
</button>
<button class="generate-btn secondary" @click="saveCurrentNumbers" :disabled="currentNumbers.front.length === 0">
<text class="btn-icon">💾</text>
<text class="btn-text">保存号码</text>
</button>
</view>
</view>
</view>
<!-- 号码分析卡片 -->
<view class="card-container" v-if="historyNumbers.length > 0">
<view class="card-header">
<view class="icon-wrapper analysis-icon">
<text class="icon">📊</text>
</view>
<view class="header-content">
<text class="card-title">号码分析</text>
<text class="card-subtitle">基于历史记录的统计分析</text>
</view>
</view>
<view class="card-body">
<view class="analysis-grid">
<view class="analysis-item">
<text class="analysis-label">总期数</text>
<text class="analysis-value">{{ historyNumbers.length }}</text>
</view>
<view class="analysis-item">
<text class="analysis-label">最热号码</text>
<text class="analysis-value">{{ getHotNumbers() }}</text>
</view>
<view class="analysis-item">
<text class="analysis-label">最冷号码</text>
<text class="analysis-value">{{ getColdNumbers() }}</text>
</view>
<view class="analysis-item">
<text class="analysis-label">平均和值</text>
<text class="analysis-value">{{ getAverageSum() }}</text>
</view>
</view>
</view>
</view>
<!-- 历史记录卡片 -->
<view class="card-container">
<view class="card-header">
<view class="icon-wrapper history-icon">
<text class="icon">📚</text>
</view>
<view class="header-content">
<text class="card-title">历史记录</text>
<text class="card-subtitle">往期开奖号码记录</text>
</view>
<button class="header-btn" @click="clearHistory" v-if="historyNumbers.length > 0">
<text class="btn-text">清空</text>
</button>
</view>
<view class="card-body">
<view class="history-list" v-if="historyNumbers.length > 0">
<view
v-for="(record, index) in historyNumbers"
:key="index"
class="history-item"
>
<view class="history-header">
<text class="period-text">第{{ record.period }}期</text>
<text class="time-text">{{ formatTime(record.time) }}</text>
</view>
<view class="history-numbers">
<view class="numbers-section">
<view class="numbers-row">
<view
v-for="(num, numIndex) in record.front"
:key="numIndex"
class="number-ball front-ball small"
>
<text class="number-text">{{ num.toString().padStart(2, '0') }}</text>
</view>
<text class="separator">+</text>
<view
v-for="(num, numIndex) in record.back"
:key="numIndex"
class="number-ball back-ball small"
>
<text class="number-text">{{ num.toString().padStart(2, '0') }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
<view class="empty-state" v-else>
<text class="empty-icon">🎱</text>
<text class="empty-text">暂无历史记录</text>
<text class="empty-desc">生成号码后会自动保存到历史记录</text>
</view>
</view>
</view>
<!-- 玩法说明卡片 -->
<view class="card-container">
<view class="card-header">
<view class="icon-wrapper info-icon">
<text class="icon">ℹ️</text>
</view>
<view class="header-content">
<text class="card-title">玩法说明</text>
<text class="card-subtitle">超级大乐透规则介绍</text>
</view>
</view>
<view class="card-body">
<view class="rule-list">
<view class="rule-item">
<text class="rule-icon">🔴</text>
<text class="rule-text">前区:从01-35中选择5个号码</text>
</view>
<view class="rule-item">
<text class="rule-icon">🔵</text>
<text class="rule-text">后区:从01-12中选择2个号码</text>
</view>
<view class="rule-item">
<text class="rule-icon">💰</text>
<text class="rule-text">每注2元,追加1元</text>
</view>
<view class="rule-item">
<text class="rule-icon">🏆</text>
<text class="rule-text">一等奖:前区5个+后区2个</text>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script>
export default {
data() {
return {
// 当前生成的号码
currentNumbers: {
front: [], // 前区5个号码
back: [] // 后区2个号码
},
// 历史记录
historyNumbers: [],
// 期数计数器
periodCounter: 1
}
},
onLoad() {
this.loadHistory()
},
onUnload() {
this.saveHistory()
},
methods: {
// 生成随机号码
generateNumbers() {
// 生成前区号码(1-35选5个)
const frontNumbers = []
while (frontNumbers.length < 5) {
const num = Math.floor(Math.random() * 35) + 1
if (!frontNumbers.includes(num)) {
frontNumbers.push(num)
}
}
frontNumbers.sort((a, b) => a - b)
// 生成后区号码(1-12选2个)
const backNumbers = []
while (backNumbers.length < 2) {
const num = Math.floor(Math.random() * 12) + 1
if (!backNumbers.includes(num)) {
backNumbers.push(num)
}
}
backNumbers.sort((a, b) => a - b)
this.currentNumbers = {
front: frontNumbers,
back: backNumbers
}
// 生成动画效果
uni.vibrateShort()
uni.showToast({
title: '号码生成成功!',
icon: 'success',
duration: 1500
})
},
// 保存当前号码到历史记录
saveCurrentNumbers() {
if (this.currentNumbers.front.length === 0) {
uni.showToast({
title: '请先生成号码',
icon: 'none'
})
return
}
const record = {
period: this.periodCounter++,
front: [...this.currentNumbers.front],
back: [...this.currentNumbers.back],
time: new Date().getTime()
}
this.historyNumbers.unshift(record)
// 最多保留50期记录
if (this.historyNumbers.length > 50) {
this.historyNumbers = this.historyNumbers.slice(0, 50)
}
this.saveHistory()
uni.showToast({
title: '号码已保存',
icon: 'success'
})
},
// 清空历史记录
clearHistory() {
uni.showModal({
title: '确认清空',
content: '确定要清空所有历史记录吗?',
success: (res) => {
if (res.confirm) {
this.historyNumbers = []
this.periodCounter = 1
this.saveHistory()
uni.showToast({
title: '已清空历史记录',
icon: 'success'
})
}
}
})
},
// 加载历史记录
loadHistory() {
try {
const history = uni.getStorageSync('lottery_history')
const counter = uni.getStorageSync('lottery_counter')
if (history) {
this.historyNumbers = history
}
if (counter) {
this.periodCounter = counter
}
} catch (e) {
console.log('加载历史记录失败:', e)
}
},
// 保存历史记录
saveHistory() {
try {
uni.setStorageSync('lottery_history', this.historyNumbers)
uni.setStorageSync('lottery_counter', this.periodCounter)
} catch (e) {
console.log('保存历史记录失败:', e)
}
},
// 格式化时间
formatTime(timestamp) {
const date = new Date(timestamp)
const now = new Date()
const diff = now.getTime() - timestamp
if (diff < 60000) return '刚刚'
if (diff < 3600000) return Math.floor(diff / 60000) + '分钟前'
if (diff < 86400000) return Math.floor(diff / 3600000) + '小时前'
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString().slice(0, 5)
},
// 获取最热号码
getHotNumbers() {
if (this.historyNumbers.length === 0) return '-'
const frequency = {}
this.historyNumbers.forEach(record => {
[...record.front, ...record.back].forEach(num => {
frequency[num] = (frequency[num] || 0) + 1
})
})
const sorted = Object.entries(frequency).sort((a, b) => b[1] - a[1])
return sorted.slice(0, 3).map(item => item[0]).join(', ')
},
// 获取最冷号码
getColdNumbers() {
if (this.historyNumbers.length === 0) return '-'
const frequency = {}
// 初始化所有号码频次为0
for (let i = 1; i <= 35; i++) {
frequency[i] = 0
}
this.historyNumbers.forEach(record => {
[...record.front, ...record.back].forEach(num => {
frequency[num] = (frequency[num] || 0) + 1
})
})
const sorted = Object.entries(frequency).sort((a, b) => a[1] - b[1])
return sorted.slice(0, 3).map(item => item[0]).join(', ')
},
// 获取平均和值
getAverageSum() {
if (this.historyNumbers.length === 0) return '-'
const totalSum = this.historyNumbers.reduce((sum, record) => {
const frontSum = record.front.reduce((a, b) => a + b, 0)
const backSum = record.back.reduce((a, b) => a + b, 0)
return sum + frontSum + backSum
}, 0)
return Math.round(totalSum / this.historyNumbers.length)
},
// 复制号码到剪贴板
copyNumbers() {
if (this.currentNumbers.front.length === 0) {
uni.showToast({
title: '请先生成号码',
icon: 'none'
})
return
}
const frontStr = this.currentNumbers.front.map(num => num.toString().padStart(2, '0')).join(' ')
const backStr = this.currentNumbers.back.map(num => num.toString().padStart(2, '0')).join(' ')
const copyText = `前区:${frontStr} 后区:${backStr}`
// #ifdef H5
if (navigator.clipboard) {
navigator.clipboard.writeText(copyText).then(() => {
uni.showToast({
title: '号码已复制',
icon: 'success'
})
}).catch(() => {
this.fallbackCopy(copyText)
})
} else {
this.fallbackCopy(copyText)
}
// #endif
// #ifdef APP-PLUS || MP-WEIXIN || MP-ALIPAY
uni.setClipboardData({
data: copyText,
success: () => {
uni.showToast({
title: '号码已复制',
icon: 'success'
})
},
fail: () => {
uni.showToast({
title: '复制失败',
icon: 'none'
})
}
})
// #endif
},
// H5环境下的备用复制方法
fallbackCopy(text) {
const textArea = document.createElement('textarea')
textArea.value = text
document.body.appendChild(textArea)
textArea.select()
try {
document.execCommand('copy')
uni.showToast({
title: '号码已复制',
icon: 'success'
})
} catch (err) {
uni.showToast({
title: '复制失败',
icon: 'none'
})
}
document.body.removeChild(textArea)
},
}
}
</script>
<style>
/* 号码头部 */
.numbers-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
}
.numbers-title {
font-size: 28rpx;
font-weight: 600;
color: #1d1d1f;
}
.copy-btn {
display: flex;
flex-direction: row;
align-items: center;
gap: 8rpx;
padding: 12rpx 20rpx;
border-radius: 20rpx;
background: linear-gradient(135deg, #4ecdc4 0%, #44a08d 100%);
border: none;
font-size: 24rpx;
color: #ffffff;
transition: all 0.2s ease;
}
.copy-btn:active {
transform: scale(0.95);
opacity: 0.8;
}
.copy-icon {
font-size: 24rpx;
}
.copy-text {
font-size: 24rpx;
font-weight: 500;
}
.lottery-page {
width: 100%;
height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
flex-direction: column;
}
/* 头部 */
.header {
padding: 60rpx 32rpx 40rpx;
text-align: center;
color: #ffffff;
}
.title {
font-size: 48rpx;
font-weight: 700;
display: block;
margin-bottom: 8rpx;
}
.subtitle {
font-size: 28rpx;
opacity: 0.9;
display: block;
}
/* 内容滚动区域 */
.content-scroll {
flex: 1;
padding: 0 24rpx 24rpx;
}
/* 卡片容器 */
.card-container {
margin-bottom: 24rpx;
border-radius: 24rpx;
background: #ffffff;
overflow: hidden;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1);
}
/* 卡片头部 */
.card-header {
display: flex;
flex-direction: row;
align-items: center;
padding: 28rpx 32rpx;
border-bottom: 1rpx solid #f0f0f5;
background: linear-gradient(135deg, #f8f9fc 0%, #ffffff 100%);
}
.icon-wrapper {
width: 72rpx;
height: 72rpx;
border-radius: 16rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
margin-right: 24rpx;
flex-shrink: 0;
}
.generate-icon {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.analysis-icon {
background: linear-gradient(135deg, #ffeaa7 0%, #fab1a0 100%);
}
.history-icon {
background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
}
.info-icon {
background: linear-gradient(135deg, #74b9ff 0%, #0984e3 100%);
}
.icon {
font-size: 36rpx;
color: #ffffff;
}
.header-content {
flex: 1;
}
.card-title {
font-size: 32rpx;
font-weight: 600;
color: #1d1d1f;
margin-bottom: 4rpx;
display: block;
}
.card-subtitle {
font-size: 24rpx;
color: #8e8e93;
display: block;
}
.header-btn {
padding: 12rpx 24rpx;
border-radius: 16rpx;
background: #f2f2f7;
border: none;
font-size: 24rpx;
color: #007aff;
}
/* 卡片内容 */
.card-body {
padding: 32rpx;
}
/* 当前号码显示 */
.current-numbers {
margin-bottom: 32rpx;
}
.numbers-section {
margin-bottom: 24rpx;
}
.section-label {
font-size: 24rpx;
color: #8e8e93;
margin-bottom: 16rpx;
display: block;
font-weight: 500;
}
.numbers-row {
display: flex;
align-items: center;
gap: 12rpx;
flex-wrap: wrap;
flex-direction: row;
}
.number-ball {
width: 64rpx;
height: 64rpx;
border-radius: 32rpx;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
}
.number-ball.small {
width: 48rpx;
height: 48rpx;
border-radius: 24rpx;
}
.front-ball {
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%);
color: #ffffff;
}
.back-ball {
background: linear-gradient(135deg, #4ecdc4 0%, #44a08d 100%);
color: #ffffff;
}
.number-text {
font-size: 24rpx;
font-weight: 600;
}
.separator {
font-size: 32rpx;
color: #8e8e93;
font-weight: 600;
margin: 0 8rpx;
}
/* 生成按钮 */
.generate-actions {
display: flex;
gap: 16rpx;
flex-direction: row;
}
.generate-btn {
flex: 1;
height: 88rpx;
border-radius: 16rpx;
border: none;
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
font-size: 28rpx;
font-weight: 600;
transition: all 0.2s ease;
}
.generate-btn.primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #ffffff;
}
.generate-btn.secondary {
background: #f2f2f7;
color: #007aff;
}
.generate-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.generate-btn:active:not(:disabled) {
transform: scale(0.95);
}
.btn-icon {
font-size: 28rpx;
}
.btn-text {
font-size: 28rpx;
}
/* 分析网格 */
.analysis-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24rpx;
}
.analysis-item {
background: #f8f9fc;
border-radius: 16rpx;
padding: 24rpx;
text-align: center;
}
.analysis-label {
font-size: 24rpx;
color: #8e8e93;
margin-bottom: 8rpx;
display: block;
}
.analysis-value {
font-size: 32rpx;
font-weight: 600;
color: #1d1d1f;
display: block;
}
/* 历史记录 */
.history-list {
max-height: 600rpx;
overflow-y: auto;
}
.history-item {
background: #f8f9fc;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 16rpx;
}
.history-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
}
.period-text {
font-size: 28rpx;
font-weight: 600;
color: #1d1d1f;
}
.time-text {
font-size: 24rpx;
color: #8e8e93;
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 60rpx 32rpx;
}
.empty-icon {
font-size: 80rpx;
display: block;
margin-bottom: 16rpx;
}
.empty-text {
font-size: 32rpx;
color: #8e8e93;
margin-bottom: 8rpx;
display: block;
}
.empty-desc {
font-size: 24rpx;
color: #c7c7cc;
display: block;
}
/* 规则说明 */
.rule-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.rule-item {
display: flex;
flex-direction: row;
align-items: center;
gap: 16rpx;
padding: 16rpx;
background: #f8f9fc;
border-radius: 12rpx;
}
.rule-icon {
font-size: 32rpx;
flex-shrink: 0;
}
.rule-text {
font-size: 28rpx;
color: #1d1d1f;
line-height: 1.4;
}
/* 响应式设计 */
@media (max-width: 750rpx) {
.content-scroll {
padding: 0 16rpx 16rpx;
}
.card-header {
padding: 24rpx 28rpx;
}
.card-body {
padding: 28rpx;
}
.analysis-grid {
grid-template-columns: 1fr;
gap: 16rpx;
}
}
</style>