mottery/frontend/src/views/NumberGenerator.vue

818 lines
22 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="number-generator">
<div class="header">
<h1>一键下单·拼盘早点</h1>
<p class="subtitle">今日拼盘新鲜出锅懒人专属AI大厨为你配号</p>
</div>
<!-- 定时任务状态区域 -->
<div class="scheduler-status-section">
<div class="status-card">
<div class="status-header">
<h3>定时任务状态</h3>
<el-button type="primary" size="small" @click="updateSchedulerStatus">刷新状态</el-button>
</div>
<div class="status-content">
<div class="status-item">
<span class="status-label">数据更新任务</span>
<span class="status-value">
<el-tag type="success" size="small">每天早上6点</el-tag>
</span>
<span class="status-time">
下次更新{{ schedulerStatus.nextUpdate ? new Date(schedulerStatus.nextUpdate).toLocaleString() : '未知' }}
</span>
</div>
<div class="status-item">
<span class="status-label">拼盘推荐任务</span>
<span class="status-value">
<el-tag type="warning" size="small">每天下午5点</el-tag>
</span>
<span class="status-time">
下次推荐{{ schedulerStatus.nextRecommend ? new Date(schedulerStatus.nextRecommend).toLocaleString() : '未知' }}
</span>
</div>
<div class="status-item">
<span class="status-label">今日推荐状态</span>
<span class="status-value">
<el-tag v-if="todayRecommend.length > 0" type="success" size="small">已生成 {{ todayRecommend.length }} 注</el-tag>
<el-tag v-else type="info" size="small">等待生成</el-tag>
</span>
</div>
</div>
</div>
</div>
<!-- 一键下单区域 -->
<div class="quick-order-section">
<div class="order-card">
<div class="card-header">
<h3>今日拼盘</h3>
<span class="lottery-type" :class="{ active: currentLotteryType === 'ssq' }">{{ currentLotteryType === 'ssq' ? '双色球' : '大乐透' }}</span>
</div>
<div class="order-content">
<div class="order-info">
<p class="order-time">下单时间:{{ todayRecommend.length ? todayRecommend[0].time : '还没开锅,快来点一份吧~' }}</p>
<p class="order-batch">拼盘编号:{{ todayRecommend.length ? todayRecommend[0].batch_id : '暂无,先点单再说~' }}</p>
</div>
<div class="order-numbers" v-if="todayRecommend.length">
<div v-for="(order, idx) in todayRecommend" :key="order.id" class="number-display multi">
<span class="order-index">{{ idx + 1 }}</span>
<span class="red-balls">
<span v-for="num in order.redNumbers" :key="num" class="ball red">{{ num.toString().padStart(2, '0') }}</span>
</span>
<span class="separator">|</span>
<span class="blue-balls">
<span v-for="num in order.blueNumbers" :key="num" class="ball blue">{{ num.toString().padStart(2, '0') }}</span>
</span>
<span class="recommend-type">{{ order.recommend_type }}</span>
</div>
</div>
<div class="no-order" v-else>
<p>今日拼盘还没出锅,快点下方按钮,开启你的好运早餐!</p>
</div>
</div>
<div class="order-actions">
<button
@click="generateNumbers"
:disabled="loading"
class="generate-btn"
>
<span v-if="loading">大厨配号中...</span>
<span v-else>来一份拼盘</span>
</button>
<button
@click="copyAllNumbers"
:disabled="loading"
class="copy-all-btn"
>
<span v-if="loading">打包中...</span>
<span v-else>打包带走</span>
</button>
</div>
</div>
</div>
<!-- 投注历史 -->
<div class="history-section">
<div class="history-header">
<h3>拼盘出锅记录</h3>
<div class="history-tabs">
<button :class="{ active: activeTab === 'ssq' }" class="tab-btn" @click="activeTab = 'ssq'">红蓝球煎饼</button>
<button :class="{ active: activeTab === 'dlt' }" class="tab-btn" @click="activeTab = 'dlt'">大乐透豆浆</button>
</div>
<button class="check-win-btn" @click="handleCheckWin" :disabled="checkingWin">
<span v-if="checkingWin">加料验收中...</span>
<span v-else>手动加料验收</span>
</button>
</div>
<div class="history-content">
<div v-if="activeTab === 'ssq'" class="tab-content">
<div v-for="(group, gIdx) in ssqHistoryGroups" :key="gIdx" class="history-group">
<div class="group-header">
<span class="batch-id">拼盘编号{{ group[0].batch_id }}</span>
<span class="issue">出锅期号{{ group[0].issue }}</span>
<span class="create-time">出锅时间{{ formatDate(group[0].created_at) }}</span>
</div>
<div class="group-numbers">
<div v-for="record in group" :key="record.id" class="group-number-item" :class="{ win: record.is_winner }">
<span class="red-balls">
<span v-for="num in record.redNumbers" :key="num" class="ball red">{{ num.toString().padStart(2, '0') }}</span>
</span>
<span class="separator">|</span>
<span class="blue-balls">
<span v-for="num in record.blueNumbers" :key="num" class="ball blue">{{ num.toString().padStart(2, '0') }}</span>
</span>
<span class="recommend-type">{{ beautifyRecommendType(record.recommend_type) }}</span>
<span v-if="record.is_winner" class="win-info">
<span class="win-level">加料成功恭喜发财</span>
<span class="win-amount">收获奖金{{ record.win_amount }}</span>
</span>
<span v-else class="not-win">未加料成功</span>
</div>
</div>
</div>
</div>
<div v-if="activeTab === 'dlt'" class="tab-content">
<div v-for="(group, gIdx) in dltHistoryGroups" :key="gIdx" class="history-group">
<div class="group-header">
<span class="batch-id">拼盘编号{{ group[0].batch_id }}</span>
<span class="issue">出锅期号{{ group[0].issue }}</span>
<span class="create-time">出锅时间{{ formatDate(group[0].created_at) }}</span>
</div>
<div class="group-numbers">
<div v-for="record in group" :key="record.id" class="group-number-item" :class="{ win: record.is_winner }">
<span class="red-balls">
<span v-for="num in record.redNumbers" :key="num" class="ball red">{{ num.toString().padStart(2, '0') }}</span>
</span>
<span class="separator">|</span>
<span class="blue-balls">
<span v-for="num in record.blueNumbers" :key="num" class="ball blue">{{ num.toString().padStart(2, '0') }}</span>
</span>
<span class="recommend-type">{{ beautifyRecommendType(record.recommend_type) }}</span>
<span v-if="record.is_winner" class="win-info">
<span class="win-level">加料成功恭喜发财</span>
<span class="win-amount">收获奖金{{ record.win_amount }}</span>
</span>
<span v-else class="not-win">未加料成功</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { recommendToday, getTodayRecommendations } from '@/api/prediction'
import { lotteryApi } from '@/api/lottery'
export default {
name: 'NumberGenerator',
data() {
return {
loading: false,
activeTab: 'ssq',
currentLotteryType: 'ssq',
todayRecommend: [],
ssqHistory: [],
dltHistory: [],
checkingWin: false,
schedulerStatus: {
nextUpdate: null,
nextRecommend: null
}
}
},
computed: {
ssqHistoryGroups() {
const groups = []
for (let i = 0; i < this.ssqHistory.length; i += 4) {
groups.push(this.ssqHistory.slice(i, i + 4))
}
return groups
},
dltHistoryGroups() {
const groups = []
for (let i = 0; i < this.dltHistory.length; i += 4) {
groups.push(this.dltHistory.slice(i, i + 4))
}
return groups
}
},
mounted() {
this.loadHistory()
this.loadTodayRecommendations()
this.setActiveTabByDay()
this.updateSchedulerStatus()
},
methods: {
getNext6am() {
const now = new Date()
const today6am = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 6, 0, 0)
if (now < today6am) {
return today6am
} else {
// 明天早上6点
return new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 6, 0, 0)
}
},
getNext5pm() {
const now = new Date()
const today5pm = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 17, 0, 0)
if (now < today5pm) {
return today5pm
} else {
// 明天下午5点
return new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 17, 0, 0)
}
},
updateSchedulerStatus() {
this.schedulerStatus = {
nextUpdate: this.getNext6am(),
nextRecommend: this.getNext5pm(),
}
},
async loadTodayRecommendations() {
try {
const response = await getTodayRecommendations()
if (response.data && response.data.success) {
this.currentLotteryType = response.data.lottery_type
this.todayRecommend = response.data.recommend.map(item => {
const { redNumbers, blueNumbers } = this.parseNumbers(item.numbers)
return {
...item,
redNumbers,
blueNumbers,
time: new Date(item.created_at).toLocaleString()
}
})
}
} catch (error) {
console.error('加载今日推荐失败:', error)
}
},
async generateNumbers() {
this.loading = true
try {
const response = await recommendToday()
if (response.data.success) {
this.currentLotteryType = response.data.lottery_type
// 解析多注推荐
this.todayRecommend = (response.data.recommend || []).map(item => {
const { redNumbers, blueNumbers } = this.parseNumbers(item.numbers)
return {
...item,
redNumbers,
blueNumbers,
time: new Date().toLocaleString()
}
})
// 刷新历史记录
this.loadHistory()
this.$message.success('拼盘出锅成功,祝你好运连连!')
}
} catch (error) {
console.error('生成号码失败:', error)
this.$message.error('大厨打盹了,再点一次试试~')
} finally {
this.loading = false
}
},
parseNumbers(numbersStr) {
if (!numbersStr) return { redNumbers: [], blueNumbers: [] }
const parts = numbersStr.split('|')
return {
redNumbers: parts[0] ? parts[0].split(',').map(n => parseInt(n)) : [],
blueNumbers: parts[1] ? parts[1].split(',').map(n => parseInt(n)) : []
}
},
async loadHistory() {
try {
// 加载双色球历史
const ssqResponse = await lotteryApi.getBetHistory('ssq', { page: 1, size: 20 })
if (ssqResponse.data.success) {
this.ssqHistory = ssqResponse.data.records.map(record => ({
...record,
...this.parseNumbers(record.numbers)
}))
}
// 加载大乐透历史
const dltResponse = await lotteryApi.getBetHistory('dlt', { page: 1, size: 20 })
if (dltResponse.data.success) {
this.dltHistory = dltResponse.data.records.map(record => ({
...record,
...this.parseNumbers(record.numbers)
}))
}
} catch (error) {
console.error('加载历史记录失败:', error)
}
},
setActiveTabByDay() {
// 自动根据今天是周几切换tab1=Monday, 7=Sunday
const weekday = new Date().getDay() || 7
if ([1, 3, 6].includes(weekday)) {
this.activeTab = 'dlt'
this.currentLotteryType = 'dlt'
} else {
this.activeTab = 'ssq'
this.currentLotteryType = 'ssq'
}
},
formatDate(dateStr) {
if (!dateStr) return ''
const date = new Date(dateStr)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})
},
async handleCheckWin() {
this.checkingWin = true
try {
const res = await lotteryApi.checkWin(this.activeTab)
if (res.data && res.data.success) {
this.$message.success('中奖比对完成!')
this.loadHistory()
} else {
this.$message.error(res.data.message || '比对失败')
}
} catch (e) {
this.$message.error('比对失败')
} finally {
this.checkingWin = false
}
},
copyAllNumbers() {
const lines = this.todayRecommend.map(item => {
const reds = item.redNumbers.map(n => n.toString().padStart(2, '0')).join(' ')
const blues = item.blueNumbers.map(n => n.toString().padStart(2, '0')).join(' ')
return `${reds} | ${blues}`
})
const text = lines.join('\n')
navigator.clipboard.writeText(text)
.then(() => {
this.$message.success('号码已打包,快去分享给小伙伴吧!')
})
.catch(() => {
this.$message.error('打包失败,厨房太忙啦~')
})
},
beautifyRecommendType(type) {
if (!type) return ''
if (type.includes('热冷')) return 'AI大厨热冷号'
if (type.includes('模式')) return '灵感模式拼盘'
if (type.includes('智能选号')) return '智能选号补足'
return type
}
}
}
</script>
<style scoped>
.number-generator {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.header {
text-align: center;
margin-bottom: 30px;
}
.header h1 {
color: #2c3e50;
margin-bottom: 10px;
font-size: 2.5rem;
}
.subtitle {
color: #7f8c8d;
font-size: 1.1rem;
}
.scheduler-status-section {
margin-bottom: 40px;
}
.status-card {
background: white;
border-radius: 16px;
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.10);
padding: 36px 36px 28px 36px;
}
.status-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 2px solid #f8f9fa;
}
.status-header h3 {
color: #2c3e50;
margin: 0;
font-size: 1.5rem;
}
.status-content {
margin-bottom: 25px;
}
.status-item {
margin-bottom: 15px;
}
.status-item .status-label {
color: #7f8c8d;
font-size: 0.95rem;
}
.status-item .status-value {
margin-left: 10px;
font-size: 0.95rem;
}
.status-item .status-time {
margin-left: 10px;
font-size: 0.95rem;
}
.quick-order-section {
margin-bottom: 40px;
}
.order-card {
background: white;
border-radius: 16px;
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.10);
padding: 36px 36px 28px 36px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 2px solid #f8f9fa;
}
.card-header h3 {
color: #2c3e50;
margin: 0;
font-size: 1.5rem;
}
.lottery-type {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 8px 16px;
border-radius: 20px;
font-size: 0.95rem;
font-weight: 500;
min-width: 70px;
text-align: center;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.15);
}
.lottery-type.active {
border: 2px solid #764ba2;
}
.order-content {
margin-bottom: 25px;
}
.order-info {
margin-bottom: 15px;
}
.order-info p {
margin: 5px 0;
color: #7f8c8d;
font-size: 0.95rem;
}
.order-numbers {
text-align: center;
padding: 20px 0 0 0;
border-radius: 8px;
}
.number-display {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
justify-content: center;
background: #f8f9fa;
border-radius: 8px;
padding: 12px 0;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.08);
}
.number-display.multi {
margin-bottom: 16px;
}
.order-index {
background: #e0e7ff;
color: #5f6ad2;
border-radius: 50%;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 1rem;
margin-right: 8px;
}
.ball {
display: inline-block;
width: 36px;
height: 36px;
border-radius: 50%;
text-align: center;
line-height: 36px;
font-weight: bold;
font-size: 1.1rem;
color: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.ball.red {
background: linear-gradient(135deg, #e74c3c, #c0392b);
}
.ball.blue {
background: linear-gradient(135deg, #3498db, #2980b9);
}
.separator {
font-size: 1.5rem;
color: #7f8c8d;
font-weight: bold;
margin: 0 15px;
}
.recommend-type {
color: #27ae60;
font-weight: 500;
margin-left: 12px;
font-size: 0.98rem;
}
.no-order {
text-align: center;
padding: 40px 20px;
color: #7f8c8d;
}
.order-actions {
text-align: center;
}
.generate-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 15px 40px;
border-radius: 25px;
font-size: 1.1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
}
.generate-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
}
.generate-btn:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.copy-all-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 15px 40px;
border-radius: 25px;
font-size: 1.1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
margin-left: 16px;
}
.copy-all-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
}
.copy-all-btn:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.history-section {
background: white;
border-radius: 16px;
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.10);
overflow: hidden;
margin-top: 40px;
}
.history-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 25px 30px;
border-bottom: 2px solid #f8f9fa;
}
.history-header h3 {
color: #2c3e50;
margin: 0;
font-size: 1.5rem;
}
.history-tabs {
display: flex;
gap: 10px;
}
.tab-btn {
background: #f8f9fa;
border: none;
padding: 10px 20px;
border-radius: 20px;
cursor: pointer;
font-weight: 500;
color: #764ba2;
}
.tab-btn.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.history-content {
padding: 30px;
}
.history-group {
background: #f6f7fa;
border-radius: 18px;
box-shadow: 0 4px 18px rgba(102,126,234,0.08);
margin-bottom: 36px;
padding: 18px 24px 16px 24px;
}
.group-header {
display: flex;
align-items: center;
gap: 32px;
margin-bottom: 12px;
font-weight: bold;
color: #2c3e50;
font-size: 1.08rem;
}
.group-numbers {
display: flex;
flex-direction: column;
align-items: flex-start;
width: 100%;
}
.group-number-item {
display: flex;
align-items: center;
gap: 12px;
background: #fff;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(102,126,234,0.08);
padding: 10px 16px;
min-width: 220px;
margin-bottom: 8px;
justify-content: flex-start;
}
.order-index {
background: #e0e7ff;
color: #5f6ad2;
border-radius: 50%;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 1rem;
margin-right: 8px;
}
.number-text {
font-size: 1.15rem;
font-family: 'Consolas', 'Menlo', 'Monaco', monospace;
color: #2c3e50;
letter-spacing: 2px;
font-weight: 600;
}
.copy-btn {
margin-left: 10px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 4px 16px;
border-radius: 16px;
font-size: 0.98rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.copy-btn:hover {
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
}
.recommend-type {
color: #27ae60;
font-weight: 500;
font-size: 0.95rem;
margin-left: 8px;
}
.check-win-btn {
margin-left: 24px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 8px 22px;
border-radius: 20px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.13);
}
.check-win-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.group-number-item.win {
border: 2px solid #27ae60;
box-shadow: 0 0 12px #27ae6040;
background: #eafaf1;
}
.win-info {
margin-left: 12px;
color: #27ae60;
font-weight: bold;
font-size: 1.05rem;
}
.win-level {
margin-right: 8px;
}
.win-amount {
background: #27ae60;
color: #fff;
border-radius: 8px;
padding: 2px 8px;
margin-left: 4px;
font-size: 0.98rem;
}
.not-win {
margin-left: 12px;
color: #aaa;
font-size: 0.98rem;
}
@media (max-width: 768px) {
.number-generator {
padding: 10px;
}
.header h1 {
font-size: 2rem;
}
.order-card {
padding: 16px;
}
.card-header {
flex-direction: column;
gap: 10px;
text-align: center;
}
.history-header {
flex-direction: column;
gap: 15px;
text-align: center;
}
.history-tabs {
width: 100%;
justify-content: center;
}
.history-content {
padding: 10px;
}
.history-group {
padding: 10px 4px 8px 4px;
}
.group-header {
flex-direction: column;
gap: 8px;
font-size: 0.98rem;
}
.group-numbers {
gap: 10px;
}
.group-number-item {
min-width: 140px;
padding: 6px 6px;
font-size: 0.9rem;
}
}
</style>