Files
auto-solution/modeler/src/views/ai/applications/gambling.vue

673 lines
24 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>
<Layout>
<a-card class="ks-page-card ks-cards-wrapper">
<template #title>
<a-space>
<span class="point"></span>
<span class="text">博弈竞赛单元运行环境</span>
</a-space>
</template>
<div class="ks-scrollable">
<a-row :gutter="15">
<a-col :span="8">
<!-- 基础配置卡片 -->
<a-card data-step="0" title="基础配置" class="ks-apps-card ks-top-apps-card">
<a-form
:model="modelDeduction"
autocomplete="off"
layout="horizontal"
:label-col="{span: 6}"
name="basic"
>
<a-form-item
label="部署模型选择"
name="deploymentModelPath"
>
<Finder :path="modelDeduction.deploymentModelPath"
@select="(p: string|null) => modelDeduction.deploymentModelPath = p" />
</a-form-item>
<a-form-item
label="对抗轮数配置"
name="competitionRound"
>
<a-input v-model:value="modelDeduction.competitionRound" placeholder="对抗轮数配置" style="width: 100%" />
</a-form-item>
<a-form-item
label="对抗场次配置"
name="competitionSession"
>
<a-input-number min="1" v-model:value="modelDeduction.competitionSession" placeholder="对抗场次配置" style="width: 100%" />
</a-form-item>
<a-form-item
label="推演倍数设置"
name="deductionMultiple"
>
<a-form-item-rest>
<a-flex>
<a-slider v-model:value="modelDeduction.deductionMultiple" :max="1000" :min="0" style="width:100%;" />
<a-input-number v-model:value="modelDeduction.deductionMultiple" :min="0" :max="1000" style="margin-left: 10px; width: 120px;" />
<span style="width: 80px; margin-left: 5px;color:#eee; line-height: 34px;"> / 1000</span>
</a-flex>
</a-form-item-rest>
</a-form-item>
<a-form-item
label="对抗模式选择"
name="competitionMode"
>
<a-select placeholder="请选择对抗模式" v-model:value="modelDeduction.competitionMode"
@change="(v: number | undefined | null)=> modelDeduction.competitionMode = v">
<a-select-option :value="1">循环赛</a-select-option>
<a-select-option :value="2">瑞士轮</a-select-option>
<a-select-option :value="3">单败淘汰赛</a-select-option>
<a-select-option :value="4">双败淘汰赛</a-select-option>
</a-select>
</a-form-item>
<a-form-item
label="并发调度配置"
name="balanceStrategy"
>
<a-select placeholder="并发调度配置" v-model:value="modelDeduction.balanceStrategy"
@change="(v: number | undefined | null)=> modelDeduction.balanceStrategy = v">
<a-select-option :value="1">均衡调度策略</a-select-option>
<a-select-option :value="2">集约调度策略</a-select-option>
<a-select-option :value="3">优先调度策略</a-select-option>
<a-select-option :value="4">触发调度策略</a-select-option>
</a-select>
</a-form-item>
</a-form>
</a-card>
<!-- 对抗配置卡片 -->
<a-card data-step="0" title="对抗配置" class="ks-apps-card ks-bottom-apps-card">
<a-form
autocomplete="off"
layout="horizontal"
:label-col="{span: 6}"
name="basic"
>
<a-form-item
label="蓝方"
name="taskName"
>
<div class="counter-wrapper">
<div class="counter-wrapper-item" v-for="i in blueCounter" :key="`blue-${i}`">
<a-row :gutter="15">
<a-col :span="19">
<Finder :only-directory="false" @select="(_p: any, f: any)=> blueNames[i] = f.name"/>
</a-col>
<a-col :span="5" v-if="i == 1">
<a-space>
<plus-circle-outlined @click="()=> add('blue')"/>
<minus-circle-outlined @click="()=> minus('blue')"/>
</a-space>
</a-col>
</a-row>
</div>
</div>
</a-form-item>
<a-form-item
label="红方"
name="t"
>
<div class="counter-wrapper">
<div class="counter-wrapper-item" v-for="i in redCounter" :key="`red-${i}`">
<a-row :gutter="15">
<a-col :span="19">
<Finder :only-directory="false" @select="(_p: any, f: any)=> redNames[i] = f.name"/>
</a-col>
<a-col :span="5" v-if="i == 1">
<a-space>
<plus-circle-outlined @click="()=> add('red')"/>
<minus-circle-outlined @click="()=> minus('red')"/>
</a-space>
</a-col>
</a-row>
</div>
</div>
</a-form-item>
</a-form>
</a-card>
</a-col>
<a-col :span="16">
<!-- 智能博弈空战卡片 -->
<a-card data-step="0" class="ks-pks-card ks-pk-apps-card">
<template #title>
<a-flex :gap="8">
<span style="margin-top: 5px;color:#eee;font-size:16px">智能博弈空战</span>
<span style="color:#999;font-size: 15px;line-height: 32px;margin-left: 40px;">
当前场次 {{ deductionPods.length }} 队伍数: {{ deductionPods.length * 2 }}
</span>
<a-button style="margin-left: auto; color:#fff;" @click="()=> startLoop()">
<a-flex>
<PlayCircleOutlined/>
<span style="margin-left: 10px;">对抗开始</span>
</a-flex>
</a-button>
<a-button style="margin-left: 15px;color:#fff;" @click="()=> rankingModelVisible = true">
<a-flex>
<OrderedListOutlined/>
<span style="margin-left: 10px;">排名统计</span>
</a-flex>
</a-button>
</a-flex>
</template>
<div class="w-full mt-2" style="margin-top: 15px;">
<a-row :gutter="15">
<a-col :span="8" v-for="(item,i) in deductionPods" :key="`pod-${item.id}`">
<a-card class="ks-pk-card" hoverable>
<template #title>
<a-flex>
<span class="ks-card-title"> {{i + 1}} </span>
</a-flex>
</template>
<div class="pk-wrapper">
<div class="pk-overlay" @click="()=> handleClickPkCard(item)"></div>
<div class="pk-teams" @click="()=> handleClickPkCard(item)">
<span class="left-team">
<a-tooltip placement="bottom">
<template #title>
{{item.blueName ?? `蓝方${i+1}`}}
</template>
{{getTeamName(item.blueName,'blue', i + 1)}}
</a-tooltip>
</span>
<span class="right-team">
<a-tooltip placement="bottom">
<template #title>
{{item.redName ?? `红方${i+1}`}}
</template>
{{getTeamName(item.redName,'red', i + 1)}}
</a-tooltip>
</span>
</div>
<div class="pk-footer">
<a-flex>
<a-progress
:percent="item.targetPercent"
:stroke-width="6"
style="margin-right: 30px;"
></a-progress>
<a-button :disabled="item.targetPercent < 100" class="pk-details-btn" size="small" @click="()=> handleDetails(item)">
对抗详情
</a-button>
</a-flex>
</div>
</div>
<template #extra class="pk-actions">
<a-space>
<span class="icon-action eye" @click="()=> handleTermStatusPopoverVisible(item)" style="margin-right: -5px;"></span>
<a-popover title="" trigger="click" v-model:open="item.termStatusPopoverVisible">
<template #content>
<a-flex>
<span style="width: 100px;text-align: right;">仿真运行状态: </span>
<span style="margin-left: 10px;">{{item.statusName ?? '-'}}</span>
</a-flex>
</template>
</a-popover>
<!-- <a-popover title="" trigger="click" v-if="item?.metricsParsed">-->
<!-- <span class="icon-action eye"></span>-->
<!-- <template #content v-if="item?.metricsParsed">-->
<!-- <a-flex>-->
<!-- <span style="width: 100px;text-align: right;">本轮总奖励: </span>-->
<!-- <span style="margin-left: 10px;">{{ item?.metricsParsed?.reward ?? '-' }}</span>-->
<!-- </a-flex>-->
<!-- <a-flex>-->
<!-- <span style="width: 100px;text-align: right;">本轮生存步数: </span>-->
<!-- <span style="margin-left: 10px;">{{ item?.metricsParsed?.steps ?? '-' }}</span>-->
<!-- </a-flex>-->
<!-- <template v-if="item?.metricsParsed?.details">-->
<!-- <a-flex>-->
<!-- <span style="width: 100px;text-align: right;">开火奖励累计值: </span>-->
<!-- <span style="margin-left: 10px;">{{ item.metricsParsed?.details.fire_reward ?? '-' }}</span>-->
<!-- </a-flex>-->
<!-- <a-flex>-->
<!-- <span style="width: 100px;text-align: right;">越界惩罚累计值: </span>-->
<!-- <span style="margin-left: 10px;">{{ item?.metricsParsed?.details.boundary_penalty ?? '-' }}</span>-->
<!-- </a-flex>-->
<!-- </template>-->
<!-- </template>-->
<!-- </a-popover>-->
<a-popconfirm
ok-text="确定"
cancel-text="取消"
@confirm="()=> confirmPause(item)"
>
<template #title>
确定{{ item.simulationStatus === 1 ? '暂停' : '开始' }}
</template>
<span :class="['icon-action', item.simulationStatus === 1 ? 'pause' : 'start']">
</span>
</a-popconfirm>
<a-popconfirm
title="确定删除?"
ok-text="确定"
cancel-text="取消"
@confirm="()=> confirmDelete(item)"
>
<span class="icon-action delete"></span>
</a-popconfirm>
</a-space>
</template>
</a-card>
</a-col>
</a-row>
</div>
</a-card>
</a-col>
</a-row>
</div>
</a-card>
<ChartsModal :deduction="selectedDeduction" :visible="chartsModalVisible" @cancel="()=> chartsModalVisible = false" />
<RankingModel :visible="rankingModelVisible" @cancel="()=> rankingModelVisible = false"/>
</Layout>
</template>
<script lang="ts" setup>
import { onMounted, onUnmounted, ref } from 'vue';
import {useRouter} from 'vue-router'
import { message } from 'ant-design-vue';
import Layout from '../layout.vue';
import { MinusCircleOutlined, OrderedListOutlined, PlayCircleOutlined, PlusCircleOutlined } from '@ant-design/icons-vue';
import RankingModel from './ranking-modal.vue';
import { createModelDeduction, deleteModelDeduction, findDeductionPodResult, runDeductionPodAfsimControl } from './api';
import type { DeductionPod, DeductionPodMetrics, ModelDeduction } from './types';
import Finder from '../finder.vue';
import { defaultModelDeduction } from './config';
import ChartsModal from './charts/charts-modal.vue';
import type { NullableString } from '@/types';
const chartsModalVisible = ref<boolean>(false);
const router = useRouter();
// 排名弹窗显隐
const rankingModelVisible = ref<boolean>(false)
// 红蓝方计数器
const redCounter = ref<number>(1);
const blueCounter = ref<number>(1);
const blueNames = ref<string[]>([])
const redNames = ref<string[]>([])
// 模型推演配置
const modelDeduction = ref<ModelDeduction>({ ...defaultModelDeduction });
// 推演Pods数据扩展类型增加进度更新定时器
const deductionPods = ref<(DeductionPod & {
animatePercent: number; // 动画进度值
targetPercent: number; // 目标进度值
stopped: boolean; // 是否暂停
progressTimer: any; // 动画定时器
updateTimer: any; // 进度更新定时器
})[]>([]);
// 心跳方法定时器标识
const heartbeatTimer = ref<any | null>(null);
const selectedDeduction = ref<DeductionPod | null | undefined>(null);
const getTeamName = (name: NullableString | undefined, side: string, level: number = 1): string => {
if(name){
if(name.length>3){
return name.substring(0,2) + '...';
}
return name;
}
return `${side === 'red' ? '红方' : '蓝方'}${level}`
}
const parseNumberPercent = (source: number|null , target: number|null) : number => {
if(source && target){
try{
return Number(Number((source/ target) * 100).toFixed(2));
} catch (e: any){
console.error(e);
}
}
return 0;
}
const handleDetails = (item: Partial<DeductionPod>) => {
// chartsModalVisible.value = true;
// selectedDeduction.value = JSON.parse(JSON.stringify(item));
router.push({
path: `/app/ai/applications/gambling/${item.id}/charts`
})
};
/**
* 随机更新进度值(核心修改:优化初始启动逻辑)
* @param pod 单个pod实例
*/
const randomUpdateProgress = (pod: any) => {
// 如果暂停或进度已到100%,直接返回
if (pod.stopped || pod.animatePercent >= 100) {
return;
}
// 随机生成下次更新时间1-3秒
const randomInterval = 1000 + Math.random() * 2000;
// 随机生成进度增长量1-5%
const randomIncrement = 1 + Math.random() * 4;
pod.updateTimer = setTimeout(() => {
// 计算新的目标进度不超过100%
pod.targetPercent = Math.min(pod.animatePercent + randomIncrement, 100);
// 启动进度动画
animateProgress(pod, pod.targetPercent);
// 递归调用,实现持续更新
randomUpdateProgress(pod);
}, randomInterval);
};
/**
* 暂停/开始进度更新
* @param item 单个pod实例
*/
const confirmPause = (item: any) => {
item.simulationStatus = item.simulationStatus === 1 ? 2 : 1;
runDeductionPodAfsimControl({
jobId: item.jobId,
afsimHostIp: item.afsimHostIp,
afsimNodePort81: item.afsimNodePort81,
type: item.simulationStatus, // 1-恢复仿真2-暂停仿真3-查询状态)
}).then(r=> {
message.info(r.msg);
if(r.code === 200) {
item.stopped = !item.stopped;
}
if(r.data?.stateDescription){
item.statusName = r.data.stateDescription;
}
})
// if (item.stopped) {
// // 暂停:清除进度更新定时器
// if (item.updateTimer) {
// clearTimeout(item.updateTimer);
// item.updateTimer = null;
// }
// message.info('已暂停');
// } else {
// // 开始:重新启动进度更新
// randomUpdateProgress(item);
// message.info('已恢复');
// }
};
const handleTermStatusPopoverVisible = (item: Partial<DeductionPod>)=> {
item.termStatusPopoverVisible = ! item.termStatusPopoverVisible;
runDeductionPodAfsimControl({
jobId: item.jobId,
afsimHostIp: item.afsimHostIp,
afsimNodePort81: item.afsimNodePort81,
type: 3, // 1-恢复仿真2-暂停仿真3-查询状态)
}).then(r=> {
message.info(r.msg);
if(r.data.stateDescription){
item.statusName = r.data.stateDescription;
}
})
}
/**
* 删除pod同时清除相关定时器
* @param item 单个pod实例
*/
const confirmDelete = (item: any) => {
console.error('confirmDelete', item);
// 清除该pod的所有定时器
if (item.updateTimer) clearTimeout(item.updateTimer);
if (item.progressTimer) clearInterval(item.progressTimer);
deleteModelDeduction(item.id as number).then(res => {
if (res.code === 200) {
message.info('删除成功.');
loadData();
}
});
};
// 进度条平滑动画函数(保留原有逻辑)
const animateProgress = (pod: any, targetPercent: number) => {
targetPercent = Math.max(0, Math.min(100, Number(targetPercent) || 0));
if (typeof pod.animatePercent !== 'number') {
pod.animatePercent = 0;
}
const step = 0.5;
if (pod.progressTimer) {
clearInterval(pod.progressTimer);
}
if (Math.abs(pod.animatePercent - targetPercent) < step) {
pod.animatePercent = targetPercent;
return;
}
pod.progressTimer = setInterval(() => {
if (pod.animatePercent < targetPercent) {
pod.animatePercent = Math.min(pod.animatePercent + step, targetPercent);
} else {
pod.animatePercent = Math.max(pod.animatePercent - step, targetPercent);
}
if (Math.abs(pod.animatePercent - targetPercent) < step) {
pod.animatePercent = targetPercent;
clearInterval(pod.progressTimer);
pod.progressTimer = null;
}
}, 30);
};
// 点击PK卡片打开链接保留原有逻辑
const handleClickPkCard = (item: DeductionPod) => {
if (item?.afsimHostIp && item?.afsimNodePort6901) {
let url = `${item.afsimHostIp}:${item.afsimNodePort6901}`;
if(!url.startsWith('http://') && !url.startsWith('https://')) {
url = 'https://' + url;
}
const newWindow = window.open(url);
if (!newWindow) {
message.warning('窗口打开失败,请检查浏览器弹窗设置');
}
} else {
message.warning('缺少IP或端口信息无法打开链接');
}
};
const refresh = () => {
if (deductionPods.value) {
deductionPods.value.forEach(podItem => {
console.info('refresh', podItem);
randomUpdateProgress(podItem);
});
}
};
// 加载数据方法核心修改确保每个pod初始化后立即启动进度更新
const loadData = () => {
findDeductionPodResult().then(r => {
let rows: DeductionPod[] = Object.values(r.data ?? []) as DeductionPod[];
const newPods: any[] = [];
rows.forEach(row=> {
let metricsParsed: DeductionPodMetrics | null = null;
try {
const metricsStr = row?.metrics?.toString() ?? '';
metricsParsed = metricsStr ? JSON.parse(metricsStr) as DeductionPodMetrics : null;
} catch (e: any) {
console.warn('解析 metrics 失败:', e);
}
// 初始化进度相关参数默认不暂停进度从0开始
const podItem = {
...row,
metricsParsed,
fake: false,
stopped: false, // 默认不暂停,页面加载后直接运行
targetPercent: parseNumberPercent(row.currentRound, row.totalRound), // 初始目标进度
animatePercent: 0, // 初始动画进度
progressTimer: null, // 动画定时器
updateTimer: null, // 进度更新定时器
statusName: null,
termStatusPopoverVisible: false,
};
newPods.push(podItem);
})
// setTimeout(() => refresh(), 1000);
deductionPods.value = newPods;
console.log('最终 deductionPods 数据:', deductionPods.value);
});
};
// 心跳方法(加载数据)
const heartbeat = () => {
loadData();
};
// 增加红蓝方计数器
const add = (side: string)=> {
if('blue' === side){
blueCounter.value ++;
} else {
redCounter.value ++;
}
};
// 减少红蓝方计数器
const minus = (side: string)=> {
if('blue' === side){
blueCounter.value = Math.max(1, blueCounter.value - 1);
blueNames.value = blueNames.value.splice(blueNames.value.length-1,1);
} else {
redCounter.value = Math.max(1, redCounter.value - 1);
redNames.value = redNames.value.splice(redNames.value.length-1,1);
}
};
// 开始对抗
const startLoop = () => {
modelDeduction.value.redNames = redNames.value.filter(v=> v).join(',')
modelDeduction.value.blueNames = blueNames.value.filter(v=> v).join(',')
createModelDeduction(modelDeduction.value).then(res => {
if (res.code === 200) {
message.success('对抗已开始');
if (heartbeatTimer.value) {
clearInterval(heartbeatTimer.value);
}
heartbeat();
heartbeatTimer.value = setInterval(() => {
heartbeat();
}, 10000);
} else if (res.msg) {
message.error(res.msg);
}
}).catch(err => {
console.error('提交配置失败:', err);
message.error('提交配置失败,请重试');
});
};
// 组件挂载时加载初始数据(页面加载完成后自动执行,触发进度模拟)
onMounted(() => {
// 页面挂载后立即加载数据数据加载完成后自动启动所有Pods的进度更新
loadData();
});
heartbeatTimer.value = setInterval(() => {
loadData();
}, 3000);
// 组件卸载时清除所有定时器(防止内存泄漏,增强版)
onUnmounted(() => {
// 清除心跳定时器
if (heartbeatTimer.value) {
clearInterval(heartbeatTimer.value);
}
// 清除所有pod的定时器
// deductionPods.value.forEach(pod => {
// if (pod.progressTimer) clearInterval(pod.progressTimer);
// if (pod.updateTimer) clearTimeout(pod.updateTimer);
// });
// 清空pods数据避免残留
deductionPods.value = [];
});
</script>
<style lang="less">
.bg-wrapper .ant-card {
&.ks-apps-card{
&.ks-top-apps-card {
margin-bottom:15px!important;
&> .ant-card-body{
height: 35vh;
overflow: hidden;
.ant-form-item{
margin-bottom:15px;
}
}
}
&.ks-bottom-apps-card{
margin-bottom:0px!important;
&> .ant-card-body{
height: 32vh;
overflow: auto;
.ant-form-item{
margin-bottom:15px;
}
}
}
}
&.ks-pk-apps-card{
&> .ant-card-body{
height: 74vh;
}
}
.counter-wrapper{
border: 1px solid #475f71;
padding: 15px;
border-radius: 2px;
.counter-wrapper-item{
margin-bottom:15px;
text-align: left;
.anticon{
color:#a2b1ba;
cursor: pointer;
font-size: 18px;
line-height: 30px;
display: block;
}
&:last-child{
margin-bottom:0;
}
}
}
}
// 进度条动画样式优化(增强平滑度)
.ant-progress-inner {
transition: width 0.05s ease-in-out;
}
.ant-progress-bg {
transition: width 0.05s ease-in-out;
}
</style>