636 lines
21 KiB
Vue
636 lines
21 KiB
Vue
<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> |