mottery/frontend/src/views/Prediction.vue

636 lines
21 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="prediction">
<el-card class="prediction-card">
<template #header>
<el-form :inline="true" class="prediction-form">
<el-form-item label="早点类型">
<el-select v-model="lotteryType" placeholder="请选择" style="width: 120px">
<el-option label="红蓝球煎饼" value="ssq" />
<el-option label="大乐斗豆浆" value="dlt" />
</el-select>
</el-form-item>
<el-form-item label="和面天数">
<el-input-number v-model="trainingPeriods" :min="10" :max="500" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="trainModel" :loading="training">
AI和面中
</el-button>
</el-form-item>
</el-form>
</template>
<div class="prediction-content">
<!-- 预测结果优先展示 -->
<div v-if="predictionResults.length > 0" class="prediction-results">
<h3>今日菜单</h3>
<el-tabs v-model="activeTab" type="border-card">
<el-tab-pane label="AI灵感煎饼" name="ml">
<div v-if="mlPrediction" class="prediction-item">
<div class="prediction-header">
<h4>AI灵感煎饼</h4>
<el-tag type="success">靠谱口感: 高</el-tag>
</div>
<div class="prediction-numbers flex-balls">
<div class="red-balls">
<span v-for="num in mlPrediction.predicted_numbers" :key="num" class="ball red-ball">
{{ num.toString().padStart(2, '0') }}
</span>
</div>
<div class="blue-balls" v-if="mlPrediction.predicted_blue || (mlPrediction.predicted_blues && mlPrediction.predicted_blues.length)" style="margin-left: 20px;">
<span v-if="mlPrediction.predicted_blue" class="ball blue-ball">
{{ mlPrediction.predicted_blue.toString().padStart(2, '0') }}
</span>
<span v-for="b in mlPrediction.predicted_blues" :key="'ml-blue-' + b" class="ball blue-ball">
{{ b.toString().padStart(2, '0') }}
</span>
</div>
</div>
<div class="prediction-info">
<p>{{ mlPrediction.confidence }}</p>
</div>
</div>
<div v-else class="no-prediction">AI今天没灵感早点卖完了</div>
</el-tab-pane>
<el-tab-pane label="老板经验豆浆" name="pattern">
<div v-if="patternPrediction" class="prediction-item">
<div class="prediction-header">
<h4>老板经验豆浆</h4>
<el-tag type="warning">靠谱口感: 中</el-tag>
</div>
<div class="prediction-numbers flex-balls" v-if="patternPrediction.predicted_numbers">
<div class="red-balls">
<span v-for="num in patternPrediction.predicted_numbers" :key="num" class="ball red-ball">
{{ num.toString().padStart(2, '0') }}
</span>
</div>
<div class="blue-balls" v-if="patternPrediction.predicted_blue || (patternPrediction.predicted_blues && patternPrediction.predicted_blues.length)" style="margin-left: 20px;">
<span v-if="patternPrediction.predicted_blue" class="ball blue-ball">
{{ patternPrediction.predicted_blue.toString().padStart(2, '0') }}
</span>
<span v-for="b in patternPrediction.predicted_blues" :key="'pattern-blue-' + b" class="ball blue-ball">
{{ b.toString().padStart(2, '0') }}
</span>
</div>
</div>
<div class="pattern-criteria">
<el-descriptions title="中奖小配方" :column="2" border>
<el-descriptions-item label="口味区间">
{{ patternPrediction.suggested_criteria.sum_range.join(' - ') }}
</el-descriptions-item>
<el-descriptions-item label="冷热搭配">
{{ patternPrediction.suggested_criteria.odd_even_ratio }}
</el-descriptions-item>
<el-descriptions-item label="配料数量">
{{ patternPrediction.suggested_criteria.zone_distribution }}
</el-descriptions-item>
<el-descriptions-item label="连号数量">
{{ patternPrediction.suggested_criteria.consecutive_count }}
</el-descriptions-item>
</el-descriptions>
</div>
</div>
<div v-else class="no-prediction">老板今天没灵感,早点卖完了</div>
</el-tab-pane>
<el-tab-pane label="拼盘早点" name="ensemble">
<div v-if="ensemblePrediction" class="prediction-item">
<div class="prediction-header">
<h4>拼盘早点</h4>
<el-tag type="info">早点花样拼盘</el-tag>
</div>
<div class="ensemble-recommendations">
<div v-for="(rec, index) in ensemblePrediction.recommendations" :key="index" class="recommendation">
<div class="recommendation-header">
<h5>{{ rec.method }}</h5>
<el-tag :type="rec.confidence === '高' ? 'success' : rec.confidence === '中' ? 'warning' : 'info'">
置信度: {{ rec.confidence }}
</el-tag>
</div>
<div class="recommendation-numbers flex-balls">
<div class="red-balls">
<span v-for="num in rec.numbers" :key="num" class="ball red-ball">
{{ num.toString().padStart(2, '0') }}
</span>
</div>
<div class="blue-balls" v-if="rec.blue || (rec.blues && rec.blues.length)">
<span v-if="rec.blue" class="ball blue-ball">
{{ rec.blue.toString().padStart(2, '0') }}
</span>
<span v-for="b in rec.blues" :key="'blue-' + b" class="ball blue-ball">
{{ b.toString().padStart(2, '0') }}
</span>
</div>
</div>
</div>
</div>
<div class="pattern-analysis">
<h5>模式分析</h5>
<el-descriptions :column="2" border>
<el-descriptions-item label="和值范围">
{{ ensemblePrediction.pattern_analysis.sum_range.join(' - ') }}
</el-descriptions-item>
<el-descriptions-item label="奇偶比">
{{ ensemblePrediction.pattern_analysis.odd_even_ratio }}
</el-descriptions-item>
<el-descriptions-item label="分区数量">
{{ ensemblePrediction.pattern_analysis.zone_distribution }}
</el-descriptions-item>
<el-descriptions-item label="连号数量">
{{ ensemblePrediction.pattern_analysis.consecutive_count }}
</el-descriptions-item>
</el-descriptions>
</div>
</div>
<div v-else class="no-prediction">拼盘早点卖完了,明天早点来</div>
</el-tab-pane>
</el-tabs>
</div>
<!-- 模型训练状态 -->
<el-alert
v-if="modelStatus"
:title="modelStatus.title"
:type="modelStatus.type"
:description="modelStatus.description"
show-icon
:closable="false"
class="model-status"
/>
<!-- 预测操作按钮区域 -->
<div class="prediction-actions">
<el-row :gutter="20" justify="center" align="middle">
<el-col :span="6">
<el-button
type="primary"
@click="predictML"
:loading="predicting"
block
>
{{ modelStatus && modelStatus.type !== 'error' ? 'AI灵感煎饼' : 'AI和面中' }}
</el-button>
</el-col>
<el-col :span="6">
<el-button
type="warning"
@click="predictPattern"
:loading="predicting"
block
>
老板经验豆浆
</el-button>
</el-col>
<el-col :span="6">
<el-button
type="info"
@click="predictEnsemble"
:loading="predicting"
block
>
{{ modelStatus && modelStatus.type !== 'error' ? '拼盘早点' : 'AI和面中' }}
</el-button>
</el-col>
</el-row>
</div>
<!-- 历史预测记录 -->
<div v-if="predictionHistory.length > 0" class="prediction-history">
<h3>昨日菜单</h3>
<el-table :data="predictionHistory" stripe>
<el-table-column prop="timestamp" label="点单时间" width="180" />
<el-table-column prop="lotteryType" label="早点类型" width="100" />
<el-table-column prop="method" label="出锅方式" width="120" />
<el-table-column prop="numbers" label="菜单号码">
<template #default="scope">
<div class="prediction-numbers flex-balls">
<div class="red-balls">
<span v-for="(num, index) in scope.row.numbers.split(' ')" :key="index" class="ball red-ball">
{{ num.padStart(2, '0') }}
</span>
</div>
<div class="blue-balls" v-if="scope.row.blue || (scope.row.blues && scope.row.blues.length)" style="margin-left: 20px;">
<span v-if="scope.row.blue" class="ball blue-ball">
{{ scope.row.blue.toString().padStart(2, '0') }}
</span>
<span v-for="b in scope.row.blues" :key="'history-blue-' + b" class="ball blue-ball">
{{ b.toString().padStart(2, '0') }}
</span>
</div>
</div>
</template>
</el-table-column>
<el-table-column prop="confidence" label="靠谱口感" width="100">
<template #default="scope">
<el-tag :type="scope.row.confidence === '高' ? 'success' : scope.row.confidence === '中' ? 'warning' : 'info'">
{{ scope.row.confidence }}
</el-tag>
</template>
</el-table-column>
</el-table>
</div>
</div>
</el-card>
</div>
</template>
<script>
import { ref, watch, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import {
trainPredictionModel,
predictNextNumbers,
getPatternPrediction,
getEnsemblePrediction
} from '@/api/prediction'
export default {
name: 'Prediction',
setup() {
const lotteryType = ref('ssq')
const trainingPeriods = ref(100)
const training = ref(false)
const predicting = ref(false)
const activeTab = ref('ml')
const modelStatus = ref(null)
const mlPrediction = ref(null)
const patternPrediction = ref(null)
const ensemblePrediction = ref(null)
const predictionResults = ref([])
const predictionHistory = ref([])
const trainModel = async () => {
training.value = true
try {
const response = await trainPredictionModel(lotteryType.value, trainingPeriods.value)
if (response.data.success) {
modelStatus.value = {
type: 'success',
title: '模型训练成功',
description: `平均准确率: ${(response.data.avg_accuracy * 100).toFixed(2)}%, 训练样本: ${response.data.training_samples}`
}
ElMessage.success('模型训练成功')
} else {
modelStatus.value = {
type: 'error',
title: '模型训练失败',
description: response.data.message
}
ElMessage.error(response.data.message)
}
} catch (error) {
console.error('训练模型失败:', error)
modelStatus.value = {
type: 'error',
title: '模型训练失败',
description: '网络错误或服务器异常'
}
ElMessage.error('训练模型失败')
} finally {
training.value = false
}
}
const predictML = async () => {
predicting.value = true
try {
// 如果模型未训练,先训练模型
if (!modelStatus.value || modelStatus.value.type === 'error') {
const trainResult = await trainPredictionModel(lotteryType.value, trainingPeriods.value)
if (!trainResult.data.success) {
ElMessage.error('模型训练失败:' + trainResult.data.message)
return
}
modelStatus.value = {
type: 'success',
title: '模型训练成功',
description: `平均准确率: ${(trainResult.data.avg_accuracy * 100).toFixed(2)}%, 训练样本: ${trainResult.data.training_samples}`
}
}
// 进行预测
const response = await predictNextNumbers(lotteryType.value, 10)
if (response.data.success) {
mlPrediction.value = response.data
activeTab.value = 'ml'
predictionResults.value = [response.data]
predictionHistory.value.unshift({
timestamp: new Date().toLocaleString(),
lotteryType: lotteryType.value === 'ssq' ? '双色球' : '大乐透',
method: '机器学习',
numbers: response.data.predicted_numbers.join(' '),
blue: response.data.predicted_blue,
blues: response.data.predicted_blues,
confidence: '高'
})
ElMessage.success('机器学习预测完成')
} else {
mlPrediction.value = null
ElMessage.error(response.data.message)
}
} catch (error) {
mlPrediction.value = null
console.error('机器学习预测失败:', error)
ElMessage.error('机器学习预测失败')
} finally {
predicting.value = false
}
}
const predictPattern = async () => {
predicting.value = true
try {
const response = await getPatternPrediction(lotteryType.value, trainingPeriods.value)
if (response.data.success) {
patternPrediction.value = response.data
activeTab.value = 'pattern'
predictionResults.value = [response.data]
// 添加到历史记录
predictionHistory.value.unshift({
timestamp: new Date().toLocaleString(),
lotteryType: lotteryType.value === 'ssq' ? '双色球' : '大乐透',
method: '模式预测',
numbers: response.data.predicted_numbers.join(' '),
blue: response.data.predicted_blue,
blues: response.data.predicted_blues,
confidence: '中'
})
ElMessage.success('模式预测完成')
} else {
patternPrediction.value = null
ElMessage.error(response.data.message)
}
} catch (error) {
patternPrediction.value = null
console.error('模式预测失败:', error)
ElMessage.error('模式预测失败')
} finally {
predicting.value = false
}
}
const predictEnsemble = async () => {
predicting.value = true
try {
// 如果模型未训练,先训练模型(因为集成预测也需要机器学习结果)
if (!modelStatus.value || modelStatus.value.type === 'error') {
const trainResult = await trainPredictionModel(lotteryType.value, trainingPeriods.value)
if (!trainResult.data.success) {
ElMessage.error('模型训练失败:' + trainResult.data.message)
return
}
modelStatus.value = {
type: 'success',
title: '模型训练成功',
description: `平均准确率: ${(trainResult.data.avg_accuracy * 100).toFixed(2)}%, 训练样本: ${trainResult.data.training_samples}`
}
}
const response = await getEnsemblePrediction(lotteryType.value, trainingPeriods.value)
if (response.data.success) {
ensemblePrediction.value = response.data
activeTab.value = 'ensemble'
predictionResults.value = [response.data]
// 为每个推荐结果添加历史记录
response.data.recommendations.forEach(rec => {
predictionHistory.value.unshift({
timestamp: new Date().toLocaleString(),
lotteryType: lotteryType.value === 'ssq' ? '双色球' : '大乐透',
method: `集成预测-${rec.method}`,
numbers: rec.numbers.join(' '),
blue: rec.blue,
blues: rec.blues,
confidence: rec.confidence
})
})
ElMessage.success('集成预测完成')
} else {
ensemblePrediction.value = null
ElMessage.error(response.data.message)
}
} catch (error) {
ensemblePrediction.value = null
console.error('集成预测失败:', error)
ElMessage.error('集成预测失败')
} finally {
predicting.value = false
}
}
const resetPrediction = () => {
modelStatus.value = null
mlPrediction.value = null
patternPrediction.value = null
ensemblePrediction.value = null
predictionResults.value = []
// 自动训练新的模型
trainModel()
}
// 监听彩票类型变化
watch(lotteryType, (newType) => {
if (newType) {
resetPrediction()
}
})
// 组件挂载时自动训练模型
onMounted(() => {
if (!lotteryType.value) {
lotteryType.value = 'ssq'
}
trainModel()
})
return {
lotteryType,
trainingPeriods,
training,
predicting,
activeTab,
modelStatus,
mlPrediction,
patternPrediction,
ensemblePrediction,
predictionResults,
predictionHistory,
trainModel,
predictML,
predictPattern,
predictEnsemble,
resetPrediction
}
}
}
</script>
<style scoped>
.prediction {
padding: 20px;
}
.prediction-card {
margin-bottom: 20px;
}
.prediction-form {
margin-bottom: 0;
display: flex;
flex-wrap: wrap;
gap: 16px;
align-items: center;
}
.prediction-content {
margin-top: 20px;
}
.model-status {
margin-bottom: 20px;
}
.prediction-results {
margin: 20px 0;
}
.prediction-item {
padding: 20px;
border: 1px solid #ebeef5;
border-radius: 4px;
margin-bottom: 20px;
}
.prediction-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.prediction-header h4 {
margin: 0;
color: #303133;
}
.prediction-numbers {
margin: 15px 0;
}
.red-balls {
display: flex;
gap: 10px;
align-items: center;
}
.ball {
display: inline-block;
width: 40px;
height: 40px;
line-height: 40px;
text-align: center;
border-radius: 50%;
font-weight: bold;
color: white;
}
.red-ball {
background-color: #ff4757;
}
.blue-ball {
background-color: #3742fa;
}
.prediction-info {
color: #606266;
font-size: 14px;
}
.pattern-criteria {
margin: 15px 0;
}
.ensemble-recommendations {
margin: 15px 0;
}
.recommendation {
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 15px;
margin-bottom: 15px;
}
.recommendation-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.recommendation-header h5 {
margin: 0;
color: #303133;
}
.recommendation-numbers.flex-balls {
display: flex;
align-items: center;
}
.red-balls, .blue-balls {
display: flex;
gap: 10px;
align-items: center;
}
.blue-balls {
margin-left: 20px;
}
.pattern-analysis {
margin-top: 20px;
}
.pattern-analysis h5 {
color: #303133;
margin-bottom: 10px;
}
.prediction-actions {
margin: 20px 0 0 0;
padding: 10px 0 0 0;
background: none;
border-radius: 0;
}
.prediction-actions .el-row {
justify-content: center;
}
.prediction-history {
margin-top: 30px;
}
.no-prediction {
color: #bbb;
text-align: center;
padding: 30px 0;
}
h3 {
color: #303133;
margin-bottom: 20px;
border-bottom: 2px solid #409eff;
padding-bottom: 10px;
}
.prediction-numbers.flex-balls {
display: flex;
align-items: center;
}
</style>