673 lines
24 KiB
Vue
673 lines
24 KiB
Vue
|
|
<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>
|