This commit is contained in:
yans 2025-11-25 17:49:53 +08:00
parent 958e11caaf
commit f065088045
42 changed files with 2297 additions and 161 deletions

View File

@ -1,9 +0,0 @@
{
"recommendations": [
"Vue.volar",
"vitest.explorer",
"dbaeumer.vscode-eslint",
"EditorConfig.EditorConfig",
"esbenp.prettier-vscode"
]
}

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
<title>雷达⽬标识别实装部署系统</title>
</head>
<body>
<div id="app"></div>

View File

@ -13,14 +13,18 @@
"format": "prettier --write src/"
},
"dependencies": {
"socket.io-client": "^4.2.0",
"@types/d3": "^7.4.3",
"ant-design-vue": "4.x",
"d3": "^7.9.0",
"echarts": "^5.6.0",
"lodash": "^4.17.21",
"pinia": "^3.0.1",
"screenfull": "^5.1.0",
"socket.io-client": "^4.2.0",
"streamsaver": "^2.0.6",
"swiper": "^12.0.3",
"vue": "^3.5.13",
"vue-router": "^4.5.0",
"screenfull": "^5.1.0"
"vue-router": "^4.5.0"
},
"devDependencies": {
"@ant-design/icons-vue": "^7.0.1",

View File

@ -19,4 +19,15 @@ export const lineStatisticsApi = async (bacthId: string, controller?: AbortContr
})
}
// 根据参数获取频谱图
export const getSpectrumImgApi = async (data: Record<string, any>, controller?: AbortController) => {
return await request({
url: `/data/spectrummap/${data.batchId}/${data.index}`,
method: 'GET',
getRaw: true,
controller: controller
})
}

View File

@ -50,3 +50,13 @@ return request({
method: 'DELETE',
})
}
// 下载数据集
export const downloadApi = async (params: Record<string, any>) => {
return request({
url: `/data/exported_data/${params.id}`,
method: 'GET',
getRaw: true,
params: params
})
}

View File

@ -41,5 +41,27 @@ export const listApi = async (params?: Record<string, any>) => {
})
}
// 给模型导入预训练权重
export const importPretrainedForModelApi = async (data: FormData) => {
const modelId = data.get('modelId')
return await request({
url: `/train/model/${modelId}/pretrained_weights`,
headers: {
Accept: 'application/json',
authorization: `Bearer ${localStorage.token}`
},
method: 'PUT',
data: data,
dataRaw: true,
})
}
export const deletePretrainedForModelApi = async (data: Record<string, any>) => {
return await request({
url: `/train/model/${data.modelId}/pretrained_weights/${data.path}`,
method: 'DELETE',
})
}

View File

@ -58,10 +58,19 @@ export const onInterruptApi = async (taskId: string) => {
})
}
// 模型导出
export const modelExportApi = async (taskId: string) => {
// 获取可导出的模型
export const getExportModelsApi = async (taskId: string) => {
return await request({
url: `/train/${taskId}/export`,
url: `/train/${taskId}/exports`,
method: 'GET'
})
}
// 模型导出
export const modelExportApi = async (data: Record<string, any>) => {
return await request({
url: `/train/${data.taskId}/export/${data.exportType}`,
method: 'GET',
getRaw: true
})
}

View File

@ -3,6 +3,6 @@ import authBg from '@/assets/images/auth/auth-bg.jpg'
export const info = {
logo: logo,
title: "雷达⽬标识别迁移部署系统",
title: "雷达⽬标识别实装部署系统",
bgImg: authBg
}

View File

@ -7,6 +7,11 @@ import { setupStore } from '@/stores'
import { router, setupRouter } from '@/router'
import { setRouteGuard } from '@/router/guard/index'
import { loadComponent, loadFunc } from '@/core/lazy_use'
import 'swiper/css';
import 'swiper/css/navigation';
import 'swiper/css/pagination';
import 'swiper/css/effect-fade';
import 'swiper/css/autoplay';
import * as Icons from '@ant-design/icons-vue'

View File

@ -16,7 +16,7 @@ export const staticRouter = [
redirect: '/model-training/index',
meta: {
icon: 'sort',
title: '模型训练',
title: '模型调优',
hidden: false,
},
component: 'ViewLayout',
@ -27,7 +27,7 @@ export const staticRouter = [
hidden: false,
meta: {
icon: 'sort',
title: '模型训练',
title: '模型调优',
hidden: true,
},
component: 'ModelTraining',
@ -38,7 +38,7 @@ export const staticRouter = [
hidden: false,
meta: {
icon: 'sort',
title: '模型训练',
title: '模型调优',
hidden: true,
},
component: 'ModelTrainingDetail',

View File

@ -18,6 +18,7 @@ import {
Space,
Button,
Input,
Textarea,
Upload,
Radio,
RadioGroup,
@ -51,8 +52,12 @@ const formRef = ref();
//
const form = ref<Record<string, any>>({
files: [],
csvFiles: [],
batch_name: "",
description: "",
work_mode: "",
system_param: "",
system_status: "",
agreement: "38解析协议",
});
@ -129,6 +134,12 @@ const onUpload = (e: File) => {
return false;
};
// csv
const onCsvUpload = (e: File) => {
form.value.csvFiles[0] = e;
return false;
};
const customAgreement = ref("");
//
@ -146,9 +157,13 @@ const reset = () => {
// formRef.value.clearValidate();
form.value = {
files: [],
csvFiles: [],
batch_name: "",
description: "",
agreement: "38解析协议",
work_mode: "",
system_param: "",
system_status: "",
// agreement: "38",
};
customAgreement.value = "";
};
@ -172,8 +187,14 @@ const onSubmit = async () => {
const formData = new FormData();
formData.append("file", form.value.files[0]);
if (form.value.csvFiles.length) {
formData.append("csvFile", form.value.csvFiles[0]);
}
formData.append("batch_name", form.value.batch_name);
formData.append("description", form.value.description);
formData.append("work_mode", form.value.work_mode);
formData.append("system_param", form.value.system_param);
formData.append("system_status", form.value.system_status);
const res = await props.api.onImportData(formData);
if (res?.code == 200) {
emits("onSuccess");
@ -188,12 +209,12 @@ const onSubmit = async () => {
v-model:open="open"
title="导入数据"
width="700px"
style="top: 200px"
style="top: 10px"
:loading="props.requestMap.importRequest.loading.value"
:onConfirm="onSubmit"
>
<Form layout="vertical" :model="form" ref="formRef" :rules="rules" v-if="open">
<Row :gutter="[20, 10]">
<Row :gutter="[20, 0]">
<Col :span="24">
<FormItem label="选择数据" name="files" class="upload-row">
<Upload
@ -213,6 +234,25 @@ const onSubmit = async () => {
<div class="upload-desc">支持bimmatjson等各类二进制原始数据及航迹数据</div>
</FormItem>
</Col>
<Col :span="24">
<FormItem label="csv数据" name="csv" class="upload-row">
<Upload
:multiple="false"
:before-upload="onCsvUpload"
:max-count="1"
style="width: 100%"
accept=".bin, .mat, .json"
>
<div class="upload-box">
<div class="upload-box-icon">
<Icon name="fileUpload" />
</div>
<div class="upload-box-title">点击上传</div>
</div>
</Upload>
<div class="upload-desc">支持bimmatjson等各类二进制原始数据及航迹数据</div>
</FormItem>
</Col>
<Col :span="24">
<FormItem label="数据名称" name="batch_name">
<Input v-model:value="form.batch_name" placeholder="请输入" />
@ -223,6 +263,27 @@ const onSubmit = async () => {
<Input v-model:value="form.description" placeholder="请输入" />
</FormItem>
</Col>
<Col :span="24">
<FormItem label="雷达工作模式" name="work_mode">
<Input v-model:value="form.work_mode" placeholder="请输入" />
</FormItem>
</Col>
<Col :span="24">
<FormItem label="雷达系统参数" name="system_param">
<Textarea
:autoSize="{
minRows: 3,
}"
v-model:value="form.system_param"
placeholder="请输入"
/>
</FormItem>
</Col>
<Col :span="24">
<FormItem label="系统状态" name="system_status">
<Input v-model:value="form.system_status" placeholder="请输入" />
</FormItem>
</Col>
<!-- <Col :span="24">
<FormItem label="数据解析" name="agreement">
<RadioGroup v-model:value="form.agreement" style="width: 100%">

View File

@ -0,0 +1,334 @@
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "ImportModal",
});
</script>
<script setup lang="ts">
import Icon from "@/components/Module/Icon/index.vue";
import Modal from "@/components/Module/Modal/index.vue";
import {
Form,
FormItem,
Select,
SelectOption,
Row,
Col,
Space,
Button,
Input,
Textarea,
Upload,
Radio,
RadioGroup,
} from "ant-design-vue";
import { ref } from "vue";
interface PropsType {
state: Record<string, any>;
api: Record<string, any>;
requestMap: Record<string, any>;
// data: Record<string, any>[];
}
const props = withDefaults(defineProps<PropsType>(), {
state: () => {
return {};
},
api: () => {
return {};
},
requestMap: () => {
return {};
},
});
const emits = defineEmits(["onSuccess"]);
//
const formRef = ref();
//
const form = ref<Record<string, any>>({
files: [],
batch_id: "",
batch_name: "",
description: "",
work_mode: "",
system_param: "",
system_status: "",
agreement: "38解析协议",
});
//
const onAgreementValid = (
rule: Record<string, any>,
value: Record<string, any>,
callback: Function,
) => {
if (
form.value.agreement &&
(form.value.agreement != customAgreement.value ||
(form.value.agreement == customAgreement.value && customAgreement.value != ""))
) {
return Promise.resolve();
} else {
return Promise.reject("请输入");
}
};
const onFilesValid = (
rule: Record<string, any>,
value: Record<string, any>,
callback: Function,
) => {
if (form.value.files && form.value.files.length) {
return Promise.resolve();
} else {
return Promise.reject("请上传文件");
}
};
const rules: Record<string, any> = {
files: [
{
required: true,
validator: onFilesValid,
message: "请上传文件",
trigger: "blur",
},
],
batch_name: [
{
required: true,
message: "请输入数据名称",
trigger: "blur",
},
],
// agreement: [
// {
// required: true,
// validator: onAgreementValid,
// message: "",
// trigger: "blur",
// },
// ],
};
//
const options = [
{
label: "38解析协议",
value: "38解析协议",
},
{
label: "14解析协议",
value: "14解析协议",
},
];
//
const onUpload = (e: File) => {
form.value.files[0] = e;
return false;
};
const customAgreement = ref("");
//
const updateAgreement = (e: Record<string, any>) => {
if (form.value.agreement == customAgreement.value) {
form.value.agreement = e.target.value;
}
customAgreement.value = e.target.value;
};
let open = ref<boolean>(false);
//
const reset = () => {
// formRef.value.clearValidate();
form.value = {
files: [],
batch_id: "",
batch_name: "",
description: "",
work_mode: "",
system_param: "",
system_status: "",
agreement: "38解析协议",
};
customAgreement.value = "";
};
//
const onOpen = (data) => {
reset();
form.value = {
batch_id: data.batch_id || "",
batch_name: data.batch_name || "",
description: data.description || "",
work_mode: data.work_mode || "",
system_param: data.system_param || "",
system_status: data.system_status || "",
};
open.value = true;
};
//
const onClose = () => {
open.value = false;
};
defineExpose({ onOpen, onClose });
//
const onSubmit = async () => {
await formRef.value.validate();
const params = {
id: form.value.batch_id,
name: form.value.batch_name,
description: form.value.description,
work_mode: form.value.work_mode,
system_param: form.value.system_param,
system_status: form.value.system_status,
};
console.log("params", params);
const res = await props.api.onUpdate(params);
if (res?.code == 200) {
emits("onSuccess");
onClose();
}
console.log("form", props);
};
</script>
<template>
<Modal
v-model:open="open"
title="更新导入数据"
width="700px"
style="top: 10px"
:loading="props.requestMap.importRequest.loading.value"
:onConfirm="onSubmit"
>
<Form layout="vertical" :model="form" ref="formRef" :rules="rules" v-if="open">
<Row :gutter="[20, 0]">
<Col :span="24">
<FormItem label="数据名称" name="batch_name">
<Input v-model:value="form.batch_name" placeholder="请输入" />
</FormItem>
</Col>
<Col :span="24">
<FormItem label="数据描述" name="description">
<Input v-model:value="form.description" placeholder="请输入" />
</FormItem>
</Col>
<Col :span="24">
<FormItem label="雷达工作模式" name="work_mode">
<Input v-model:value="form.work_mode" placeholder="请输入" />
</FormItem>
</Col>
<Col :span="24">
<FormItem label="雷达系统参数" name="system_param">
<Textarea
:autoSize="{
minRows: 3,
}"
v-model:value="form.system_param"
placeholder="请输入"
/>
</FormItem>
</Col>
<Col :span="24">
<FormItem label="系统状态" name="system_status">
<Input v-model:value="form.system_status" placeholder="请输入" />
</FormItem>
</Col>
<!-- <Col :span="24">
<FormItem label="数据解析" name="agreement">
<RadioGroup v-model:value="form.agreement" style="width: 100%">
<Row :gutter="[12, 12]">
<Col :span="12" v-for="item in options">
<Radio :value="item.value">{{ item.label }}</Radio>
</Col>
<Col :span="12">
<Radio :value="customAgreement">
<Input :value="customAgreement" @change="updateAgreement" />
</Radio>
</Col>
</Row>
</RadioGroup>
</FormItem>
</Col> -->
</Row>
</Form>
</Modal>
</template>
<style scoped lang="scss">
.grid-box {
display: flex;
align-items: center;
justify-content: center;
white-space: nowrap;
:deep(.ant-form-item) {
margin-bottom: 0;
}
}
.grey-text {
color: #8c8c8c;
}
.upload-row {
.ant-upload-wrapper {
display: block;
:deep(.ant-upload) {
width: 100%;
}
}
.ant-upload-select {
width: 100%;
.upload-box {
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
font-size: 16px;
color: #8a8a8a;
width: 100%;
aspect-ratio: 537 / 91;
border-radius: 6px 6px 6px 6px;
border: 1px solid #d9d9d9;
background: rgba(200, 200, 200, 0.2);
&-icon {
}
&-title {
}
}
}
.upload-desc {
font-family:
Source Han Sans,
Source Han Sans;
font-weight: 400;
font-size: 12px;
color: #c8c8c8;
line-height: 26px;
text-align: justify;
font-style: normal;
text-transform: none;
}
}
.upload-footer {
padding: 12px 16px;
width: 100%;
display: flex;
align-items: center;
justify-content: end;
gap: 20px;
}
</style>

View File

@ -8,8 +8,9 @@ export default defineComponent({
<script setup lang="ts">
import ModuleCard from "@/views/dataManage/index/components/Card/ModuleCard.vue";
import Chat from "@/views/dataManage/index/components/Chat/chat.vue";
import SpectrumDiagramChatCarouse from "@/views/dataManage/index/components/Module/SpectrumDiagramChatCarouse.vue";
import { Select, SelectOption, Spin } from "ant-design-vue";
import { onMounted, onUnmounted, ref, computed } from "vue";
import { onMounted, onUnmounted, ref, computed, unref } from "vue";
interface PropsType {
statistics: Record<string, any>;
@ -55,13 +56,20 @@ const onBatchChange = async (e: string | any, ...args: any) => {
}
};
const loading = ref(false);
const loading = computed(() => {
return (
props.statistics.requestMap.lineStatisticsRequest.loading.value ||
props.statistics.requestMap.radarStatisticsRequest.loading.value ||
props.statistics.state.spectrumImgsLoading.value
);
});
const panel = ref<string | number>("radar");
const panel = ref<string>("radar");
const panelOptions = ref([
{ label: "雷达图", value: "radar" },
{ label: "折线图", value: "line" },
{ label: "频谱图", value: "spectrumDiagram" },
]);
const onPanelChange = async (e: string | any, ...args: any) => {
@ -69,12 +77,14 @@ const onPanelChange = async (e: string | any, ...args: any) => {
};
const updateChat = async () => {
props.statistics.api.radarStopScanAnimation();
if (panel.value == "radar") {
await props.statistics.api.onUpdateRadarChat(batchId.value);
props.statistics.api.radarStartScanAnimation(echartRef);
} else if (panel.value == "line") {
props.statistics.api.radarStopScanAnimation();
await props.statistics.api.onUpdateLineChat(batchId.value);
} else if (panel.value == "spectrumDiagram") {
await props.statistics.api.onUpdateSpectrumDiagramChat(batchId.value);
}
};
@ -88,6 +98,15 @@ const showOptions = computed(() => {
}
});
const showData = computed(() => {
if (panel.value === "spectrumDiagram") {
console.log("spectrumDiagramChatImgs", props.statistics.state.spectrumDiagramChatImgs.value);
return props.statistics.state.spectrumDiagramChatImgs.value;
} else {
return showOptions.value;
}
});
onMounted(() => {
console.log("props", props.statistics.requestMap);
});
@ -102,10 +121,7 @@ const prefix = "data-view-part-one";
<template>
<div :class="prefix">
<ModuleCard
title="数据可视化"
:loading="props.statistics.requestMap.lineStatisticsRequest.loading.value"
>
<ModuleCard title="数据可视化" :loading="loading">
<template #head-extra>
<Select
placeholder="请选择类型"
@ -130,7 +146,10 @@ const prefix = "data-view-part-one";
style="margin-left: 20px; width: 240px"
:loading="props.statistics.requestMap.exportDataListRequest.loading.value"
>
<template v-if="loading" #notFoundContent>
<template
v-if="props.statistics.requestMap.exportDataListRequest.loading.value"
#notFoundContent
>
<Spin size="small" />
</template>
<SelectOption
@ -140,7 +159,12 @@ const prefix = "data-view-part-one";
>
</Select>
</template>
<Chat :options="showOptions" @init="echartInit" />
<template v-if="['radar', 'line'].includes(unref(panel))">
<Chat :options="showData" @init="echartInit" />
</template>
<template v-else>
<SpectrumDiagramChatCarouse :imgs="showData" />
</template>
</ModuleCard>
</div>
</template>

View File

@ -77,7 +77,7 @@ const columns = [
{
title: "真值标注",
dataIndex: "label_info",
width: "30%",
width: "20%",
},
{
title: "标注时间",
@ -192,6 +192,13 @@ const rowSelection = {
//
const operateList = [
{
name: "数据集全选/全不选",
icon: "chat",
key: "selectAll",
customClass: "custom-btn",
disabled: false,
},
{
name: "导入标注",
icon: "import",
@ -218,6 +225,25 @@ const exportData = () => {
exportModalRef.value.onOpen(unref(colSelectIds));
};
const onToggleSelectAllData = () => {
const useListIds = useList.value.map((item) => item.track_id);
let selectDataIds = JSON.parse(JSON.stringify(props.selectDataIds));
const isSelectAll = useListIds.every((item) => {
return selectDataIds.includes(item);
});
console.log("isSelectAll", isSelectAll);
if (isSelectAll) {
selectDataIds = selectDataIds.filter((item) => !useListIds.includes(item));
} else {
useListIds.forEach((item) => {
if (!selectDataIds.includes(item)) {
selectDataIds.push(item);
}
});
}
emits("update:selectDataIds", selectDataIds);
};
const importDataLabelModalRef = ref();
//
@ -231,6 +257,7 @@ const onMenuSelect = (item: Record<string, any>) => {
const eventMap: Record<string, (...args: any[]) => any> = {
export: exportData,
import: onShowImpotModal,
selectAll: onToggleSelectAllData,
};
if (eventMap[item.key]) {
eventMap[item.key]();

View File

@ -8,6 +8,7 @@ export default defineComponent({
<script setup lang="ts">
import Icon from "@/components/Module/Icon/index.vue";
import ImportModal from "@/views/dataManage/index/components/Modal/ImportModal.vue";
import ImportEditModal from "@/views/dataManage/index/components/Modal/importEditModal.vue";
import ModuleCard from "@/views/dataManage/index/components/Card/ModuleCard.vue";
import { ref, h, unref, computed, watch, reactive, onMounted } from "vue";
import { Button, Spin, message, Modal, Space, Tooltip, Input } from "ant-design-vue";
@ -71,17 +72,37 @@ const columns = [
{
title: "数据名称",
dataIndex: "batch_name",
width: "35%",
width: "15%",
},
{
title: "数据来源",
dataIndex: "batch_id",
width: "15%",
width: "10%",
},
{
title: "数据描述",
dataIndex: "description",
width: "30%",
width: "10%",
},
{
title: "总数量",
dataIndex: "point_count",
width: "7%",
},
{
title: "雷达工作模式",
dataIndex: "work_mode",
width: "15%",
},
{
title: "雷达系统参数",
dataIndex: "system_param",
width: "15%",
},
{
title: "系统状态",
dataIndex: "system_status",
width: "15%",
},
// {
// title: "",
@ -91,12 +112,12 @@ const columns = [
{
title: "数据大小",
dataIndex: "size_mb",
width: "10%",
width: "8%",
},
{
title: "操作",
dataIndex: "operate",
width: "10%",
width: "5%",
},
];
@ -158,12 +179,12 @@ const operateList = [
key: "import",
customClass: "custom-btn",
},
{
name: "接收数据",
icon: "refresh",
key: "receive",
customClass: "custom-btn grey-btn",
},
// {
// name: "",
// icon: "refresh",
// key: "receive",
// customClass: "custom-btn grey-btn",
// },
// {
// name: "",
// icon: "save",
@ -216,6 +237,14 @@ const onShowImpotModal = () => {
importModalRef.value.onOpen();
};
//
const importEditModalRef = ref();
//
const onShowImpotEditModal = (info: Record<string, any>) => {
importEditModalRef.value.onOpen(info);
};
//
const onPageChange = async (options: Record<string, any>) => {
const { current, pageSize } = options;
@ -421,8 +450,26 @@ const prefix = "data-import";
>
<template #bodyCell="{ column, text, index, record }">
<!-- 数据名称 -->
<template v-if="['batch_name', 'description'].includes(column.dataIndex)">
<div class="marker-row" v-if="editIndex == index && editField == column.dataIndex">
<template v-if="['batch_id'].includes(column.dataIndex)">
<Tooltip :title="text">
<div class="marker-text">
{{ text }}
</div>
</Tooltip>
</template>
<template
v-if="
[
'batch_name',
'description',
'point_count',
'work_mode',
'system_param',
'system_status',
].includes(column.dataIndex)
"
>
<div v-if="editIndex == index && editField == column.dataIndex">
<div class="marker-input">
<Input :value="editText" @change="onUpdateEditText" />
</div>
@ -449,8 +496,19 @@ const prefix = "data-import";
</Space>
</div>
</div>
<div class="marker-row" v-else @click="onSetEditIndex(index, column.dataIndex)">
<div class="marker-text">{{ text }}</div>
<div
:class="{
'marker-row': true,
'marker-row-limit': 'batch_name' == column.dataIndex,
}"
v-else
@click="onShowImpotEditModal(record)"
>
<Tooltip :title="text">
<div class="marker-text">
{{ text }}
</div>
</Tooltip>
</div>
</template>
<!-- 时间格式化 -->
@ -459,7 +517,11 @@ const prefix = "data-import";
</template>
<!-- 数据描述 -->
<template v-if="column.dataIndex === 'size_mb'"> {{ text }} MB </template>
<template v-if="column.dataIndex === 'size_mb'">
<Tooltip :title="`${text}MB`">
<div class="marker-text">{{ text }} MB</div>
</Tooltip>
</template>
<!-- 编辑 -->
<template v-if="column.dataIndex === 'operate'">
<Space :size="10">
@ -516,6 +578,15 @@ const prefix = "data-import";
requestMap: props.dataImport.requestMap,
}"
/>
<ImportEditModal
ref="importEditModalRef"
@onSuccess="afterImport"
v-bind="{
api: props.dataImport.api,
state: props.dataImport.state,
requestMap: props.dataImport.requestMap,
}"
/>
</template>
<style lang="scss" scoped>
@ -543,6 +614,7 @@ $prefix: "data-import";
}
.marker-row {
width: 100%;
height: 100%;
max-width: 500px;
display: flex;
align-items: center;
@ -553,6 +625,9 @@ $prefix: "data-import";
display: block;
}
}
&-limit {
width: calc(100% - 50px);
}
}
.marker-input {
flex: 1;

View File

@ -0,0 +1,74 @@
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "SpectrumDiagramChatCarouse",
});
</script>
<script setup lang="ts">
import { Swiper, SwiperSlide } from "swiper/vue";
import { Pagination, Navigation, Autoplay } from "swiper/modules";
import { Carousel } from "ant-design-vue";
interface PropsType {
imgs: string[];
}
const props = withDefaults(defineProps<PropsType>(), {
imgs: () => [],
});
const modules = [Pagination, Navigation, Autoplay];
const prefix = "spectrum-diagram-chat";
</script>
<template>
<div
:class="prefix"
:style="{
'--swiper-navigation-size': '24px',
}"
>
<swiper
:modules="modules"
:slides-per-view="1"
:space-between="30"
:pagination="{ clickable: true }"
:navigation="true"
class="chat-img-swiper"
>
<swiper-slide v-for="(img, index) in props.imgs" :key="index">
<div class="slide-content">
<img :src="img" />
</div>
</swiper-slide>
</swiper>
</div>
</template>
<style lang="scss" scoped>
$prefix: "spectrum-diagram-chat";
.#{$prefix} {
width: 100%;
height: 100%;
/* For demo */
.slide-content {
width: 100%;
height: 100%;
max-width: 100%;
max-height: 100%;
display: flex;
align-items: center;
justify-content: center;
img {
width: auto;
height: 100%;
}
}
.chat-img-swiper {
width: 100%;
height: 100%;
}
}
</style>

View File

@ -77,6 +77,24 @@ const columns = [
},
];
const downloadMethod = [
{
name: "导出为json",
type: "json",
key: "download_json",
},
{
name: "导出为csv",
type: "csv",
key: "download_csv",
},
{
name: "导出为mat",
type: "mat",
key: "download_mat",
},
];
const loading = computed(() => {
return (
props.requestMap.testDatasetListRequest.loading.value ||
@ -204,8 +222,12 @@ const onShowDelete = (info: Record<string, any>) => {
//
const onDownload = (info: Record<string, any>) => {
console.log("onDownload", info);
const fileId = info.id;
const { name, id, type } = info;
props.api.onDatasetDownload({
id: id,
type: type,
fileName: name + `测试集.${type}`,
});
};
const tableContainRef = ref();
@ -299,16 +321,33 @@ const prefix = "dataset-test";
<template v-if="column.dataIndex == 'timestamp'">
{{ dayjs(text).format("YYYY-MM-DD HH:mm:ss") }}
</template>
<!-- 删除 -->
<!-- 操作 -->
<template v-if="column.dataIndex === 'operate'">
<Space :size="10">
<Tooltip title="保存" class="primary-btn">
<Button type="text" @click="onDownload(record)">
<Popover
arrow-point-at-center
:getPopupContainer="(e: HTMLElement) => props.pageRef"
>
<template #content>
<div
v-for="opt in downloadMethod"
class="download-option"
@click="
onDownload({
...record,
type: opt.type,
})
"
>
{{ opt.name }}
</div>
</template>
<Button type="text" class="primary-btn">
<template #icon>
<Icon name="save" />
</template>
</Button>
</Tooltip>
</Popover>
<Tooltip title="删除" class="delete-btn">
<Button type="text" @click="onShowDelete(record)">
<template #icon>
@ -412,5 +451,12 @@ $prefix: "dataset-test";
border-start-end-radius: 0px;
}
}
.download-option {
padding: 4px;
cursor: pointer;
&:hover {
color: #1677ff;
}
}
}
</style>

View File

@ -77,6 +77,24 @@ const columns = [
},
];
const downloadMethod = [
{
name: "导出为json",
type: "json",
key: "download_json",
},
{
name: "导出为csv",
type: "csv",
key: "download_csv",
},
{
name: "导出为mat",
type: "mat",
key: "download_mat",
},
];
const loading = computed(() => {
props.requestMap.trainDatasetListRequest.loading.value ||
props.requestMap.deleteRequest.loading.value ||
@ -188,8 +206,12 @@ const onShowDelete = (info: Record<string, any>) => {
//
const onDownload = (info: Record<string, any>) => {
console.log("onDownload", info);
const fileId = info.id;
const { name, id, type } = info;
props.api.onDatasetDownload({
id: id,
type: type,
fileName: name + `训练集.${type}`,
});
};
//
@ -298,13 +320,31 @@ const prefix = "dataset-test";
<!-- 删除 -->
<template v-if="column.dataIndex === 'operate'">
<Space :size="10">
<Tooltip title="保存" class="primary-btn">
<Button type="text" @click="onDownload(record)">
<Popover
arrow-point-at-center
:getPopupContainer="(e: HTMLElement) => props.pageRef"
>
<template #content>
<div
v-for="opt in downloadMethod"
class="download-option"
@click="
onDownload({
...record,
type: opt.type,
})
"
>
{{ opt.name }}
</div>
</template>
<Button type="text" class="primary-btn">
<template #icon>
<Icon name="save" />
</template>
</Button>
</Tooltip>
</Popover>
<Tooltip title="删除" class="delete-btn">
<Button type="text" @click="onShowDelete(record)">
<template #icon>

View File

@ -272,6 +272,7 @@ $prefix: "_table-comp";
align-items: start;
justify-content: start;
background-color: #fff;
overflow: auto;
&-col {
padding: 16px;
font-size: 16px;
@ -283,6 +284,7 @@ $prefix: "_table-comp";
align-items: center;
justify-content: start;
// line-height: 32px;
flex-shrink: 0;
&::after {
content: "";
position: absolute;
@ -301,6 +303,7 @@ $prefix: "_table-comp";
}
&-body {
width: 100%;
overflow: auto;
&-row {
display: flex;
align-items: start;
@ -313,6 +316,8 @@ $prefix: "_table-comp";
display: flex;
align-items: start;
justify-content: start;
align-self: stretch;
flex-shrink: 0;
gap: 10px;
}
}

View File

@ -1,4 +1,4 @@
import { datasetListApi, deleteDatasetApi, updateDatasetApi } from '@/apis/dataset/index.ts'
import { datasetListApi, deleteDatasetApi, updateDatasetApi, downloadApi } from '@/apis/dataset/index.ts'
import { ref } from 'vue'
@ -65,6 +65,10 @@ export const useDatasetHooks = () => {
return res
}
const downloadRequest = async (params: Record<string, any>) => {
return await downloadApi(params)
}
return {
testDatasetListRequest: {
@ -82,6 +86,9 @@ export const useDatasetHooks = () => {
updateRequest: {
request: updateRequest,
loading: updateLoading
},
downloadRequest: {
request: downloadRequest
}
}
}

View File

@ -0,0 +1,103 @@
import { ref } from "vue";
export const useSpectrumDiagramChatHooks = () => {
const visualColors = ['#5470C6', '#91CC75', '#EE6666', '#73C0DE', '#FAC858', '#3BA272', '#FC8452', '#9A60B4', '#EA7CCC'];
const options = ref<Record<string, any>>({
// title: {
// text: `{a|总量}\n{b|0}`,
// textStyle: {
// rich: {
// a: {
// fontSize: 14,
// color: "#4E5969",
// lineHeight: 24,
// },
// b: {
// fontSize: 16,
// color: "#1D2129",
// lineHeight: 28,
// fontWeight: "bold",
// },
// },
// },
// left: "50%", // 水平居中
// top: "50%", // 垂直居中
// textAlign: "center",
// textVerticalAlign: "middle",
// },
legend: {
icon: "circle",
bottom: "0",
left: "center",
itemWidth: 6,
itemGap: 20,
textStyle: {
color: "#556677",
},
},
// tooltip: {
// trigger: "item",
// },
grid: {
left: '10%',
right: '10%',
bottom: '10%',
top: '5%',
containLabel: true
},
xAxis: {
type: 'category',
data: [],
name: 'Time (s)',
nameLocation: 'middle',
nameGap: 30,
axisLabel: {
interval: 10 // 间隔显示标签,避免拥挤
}
},
yAxis: {
type: 'category',
data: [],
axisLabel: {
interval: 10 // 间隔显示标签
}
},
visualMap: {
type: 'continuous',
min: -60,
max: 20,
color: visualColors,
orient: 'vertical',
right: 0,
top: 'center',
itemWidth: 20,
itemHeight: 150,
label: {
formatter: function(value: any) {
return value;
}
}
},
series: [
{
type: 'heatmap',
data: [],
pointSize: 2,
emphasis: {
itemStyle: {
borderColor: '#333',
borderWidth: 1
}
},
// progressive: 1000,
// animation: false
}
],
})
return {
options
}
}

View File

@ -1,5 +1,5 @@
import { statisticsApi, lineStatisticsApi } from '@/apis/dataManage/statistics.ts'
import { statisticsApi, lineStatisticsApi, getSpectrumImgApi } from '@/apis/dataManage/statistics.ts'
import { ref } from 'vue'
export const useMain = () => {
@ -37,6 +37,14 @@ export const useMain = () => {
}
const getSpectrumImg = async (data: Record<string, any>) => {
const res = await getSpectrumImgApi(data).catch(err => {
return err
})
return res
}
return {
statisticsRequest: {
@ -47,6 +55,9 @@ export const useMain = () => {
request: getLineStatistics,
loading: lineLoading,
interrupt: onLineStatisticsInterrupt
},
getSpectrumImgRequest: {
request: getSpectrumImg
}
}
}

View File

@ -113,6 +113,8 @@ $prefix: "data-manage";
}
&-grid {
flex: 1;
flex-shrink: 0;
overflow: hidden;
background: rgba(255, 255, 255, 0.6);
border: 1px solid rgba(231, 239, 251, 0.5);
border-radius: 10px;
@ -130,8 +132,10 @@ $prefix: "data-manage";
:deep(.wrapper) {
display: flex;
gap: 10px;
width: 470px;
width: 500px;
flex-wrap: wrap;
max-height: 600px;
overflow: auto;
}
:deep(.index-box) {
width: 150px;
@ -157,5 +161,16 @@ $prefix: "data-manage";
font-size: 14px;
}
}
:deep(.download-option) {
padding: 8px 4px;
cursor: pointer;
border-bottom: 1px solid #d9d9d9;
&:hover {
color: #1677ff;
}
&:last-child {
border-bottom: none;
}
}
}
</style>

View File

@ -1,8 +1,9 @@
import { useDatasetHooks } from '@/views/dataManage/index/hooks/dataset.ts'
import { ref } from 'vue'
import { downloadFile } from '@/utils/download/index.ts'
export const useDatasetServer = () => {
const { testDatasetListRequest, trainDatasetListRequest, deleteRequest, updateRequest } = useDatasetHooks()
const { testDatasetListRequest, trainDatasetListRequest, deleteRequest, updateRequest, downloadRequest } = useDatasetHooks()
const testDataset = ref([])
const getTestDataset = async () => {
@ -32,6 +33,20 @@ export const useDatasetServer = () => {
return res
}
const onDatasetDownload = async (data: Record<string, any>) => {
const { fileName, type, id } = data
const res = await downloadRequest.request(data)
console.log('onDatasetDownload', res)
// if (res.status != 200) return res
const blob = await res.blob()
const hrefUrl = window.URL.createObjectURL(blob) // 创建下载的链接
downloadFile(hrefUrl, fileName);
return res
}
const state = {
testDataset,
@ -42,7 +57,8 @@ export const useDatasetServer = () => {
getTestDataset,
getTrainDataset,
deleteDataset,
updateDataset
updateDataset,
onDatasetDownload
}
const requestMap = {

View File

@ -3,10 +3,12 @@ import { useMain as useCircleChatMain } from "@/views/dataManage/index/hooks/cir
import { useMain as useLineChatMain } from "@/views/dataManage/index/hooks/lineChat.ts";
import { useMain as useDataExportMain } from "@/views/dataManage/index/hooks/dataExport.ts";
import { useMain as useRadarChatMain } from "@/views/dataManage/index/hooks/radarChat.ts";
import { useSpectrumDiagramChatHooks } from '@/views/dataManage/index/hooks/spectrumDiagramChat.ts'
import { ref } from "vue";
export const useMain = () => {
const { options: spectrumDiagramChatOptions } = useSpectrumDiagramChatHooks()
const { options: circleChatOptions } = useCircleChatMain();
const {
listRequest: exportDataListRequest,
@ -28,8 +30,9 @@ export const useMain = () => {
stopScanAnimation: radarStopScanAnimation,
} = useRadarChatMain();
const { statisticsRequest, lineStatisticsRequest } = useStatisticsMain();
const { statisticsRequest, lineStatisticsRequest, } = useStatisticsMain();
const { lineStatisticsRequest: radarStatisticsRequest } = useStatisticsMain();
const { lineStatisticsRequest: SpectrumDiagramStatisticsRequest, getSpectrumImgRequest } = useStatisticsMain();
const statisticsData = ref({});
@ -73,7 +76,6 @@ export const useMain = () => {
const lineStatisticsData = ref({});
const getLineStaticsData = async (batchId: string) => {
const res = await lineStatisticsRequest.request(batchId);
console.log("getLineStaticsData", res);
if (res.code == 200) {
lineStatisticsData.value = res.data;
}
@ -111,13 +113,94 @@ export const useMain = () => {
};
const onUpdateLineChat = async (batchId: string) => {
if (!batchId) return;
await getLineStaticsData(batchId);
updateLineChatOptions(lineStatisticsData.value);
};
// 频谱图
const spectrumDiagramChatData = ref({});
const spectrumDiagramChatImgs = ref<string[]>([])
const spectrumImgsLoading = ref(false)
const getSpectrumDiagramChatData = async (batchId: string) => {
if (!batchId) return;
const res = await SpectrumDiagramStatisticsRequest.request(batchId);
if (res?.code == 200) {
spectrumDiagramChatData.value = res.data;
}
}
// 更新图谱数据
const updateChatImgs = async (batchId: string, rawData: Record<string, any>) => {
const useJem = (rawData?.jem || []).slice(0, 5);
spectrumDiagramChatImgs.value = []
await Promise.all([
await useJem.map(async (item: Record<string, any>, index: number) => {
const params = {
batchId: batchId,
index: index,
}
const jemRes = await getSpectrumImgRequest.request(params)
console.log('jemRes', jemRes)
const imgBlob = await jemRes.blob()
// 回显为base64图片
return new Promise(resolve => {
const reader = new FileReader();
reader.readAsDataURL(imgBlob);
reader.onload = function () {
spectrumDiagramChatImgs.value.push(reader.result);
resolve(void 0)
};
})
})
])
// spectrumDiagramChatImgs.value = imgs
}
// 频谱图
const onUpdateSpectrumDiagramChatOptions = (chatData: Record<string, any>) => {
const { jem } = chatData
console.log('jem', jem)
// const imax = jem.length
// const jmax = 80
const data = jem.data
const xData = jem.xData
const yData = jem.yData
// for (let i = 0; i < jem.length; i++) {
// for (let j = 0; j < jem[i].length; j++) {
// data.push([jem[i][j]]);
// }
// }
console.log('data', data)
spectrumDiagramChatOptions.value.series[0].data = data;
spectrumDiagramChatOptions.value.yAxis.data = yData;
spectrumDiagramChatOptions.value.xAxis.data = xData;
spectrumDiagramChatOptions.value = {
...spectrumDiagramChatOptions.value,
};
}
const onUpdateSpectrumDiagramChat = async (batchId: string) => {
if (!batchId) return;
spectrumImgsLoading.value = true
await getSpectrumDiagramChatData(batchId);
await updateChatImgs(batchId, spectrumDiagramChatData.value)
spectrumImgsLoading.value = false
// onUpdateSpectrumDiagramChatOptions(spectrumDiagramChatData.value);
}
// 根据batchId获取雷达图统计数据
const radarStatisticsData = ref({});
const getRadatStaticsData = async (batchId: string) => {
if (!batchId) return;
const res = await radarStatisticsRequest.request(batchId);
if (res.code == 200) {
radarStatisticsData.value = res.data;
@ -183,15 +266,21 @@ export const useMain = () => {
lineChatOptions,
radarStatisticsData: radarStatisticsData,
radarChatOptions,
spectrumDiagramChatOptions: spectrumDiagramChatOptions,
spectrumDiagramChatImgs,
spectrumImgsLoading,
exportData: exportData,
};
const api = {
getStatisticsData,
updateCircleChatOptions,
onUpdateSpectrumDiagramChatOptions,
getSpectrumDiagramChatData,
onUpdateChat,
onUpdateLineChat,
onUpdateRadarChat,
onUpdateSpectrumDiagramChat,
radarStartScanAnimation,
radarStopScanAnimation,
getExportData,

View File

@ -356,7 +356,7 @@ const onStart = async () => {
await logCleanup();
const logRes = await props.api.log.getLogs(taskId);
logRead = logRes.body?.getReader();
onStartTimeCount();
onStartStatusTimeCount();
startLogTimecount();
}
};
@ -378,7 +378,7 @@ const onStop = async () => {
props.api.cleanTaskId();
//
clearLogTimecount();
onCleanTimeCount();
onCleanStatusTimeCount();
//
props.api.resetTaskStatus();
props.api?.getTaskList();
@ -388,7 +388,7 @@ const onStop = async () => {
let timeCount: any = undefined;
//
const onCleanTimeCount = () => {
const onCleanStatusTimeCount = () => {
if (timeCount) {
clearInterval(timeCount);
timeCount = undefined;
@ -396,15 +396,15 @@ const onCleanTimeCount = () => {
};
//
const onStartTimeCount = () => {
onCleanTimeCount();
const onStartStatusTimeCount = () => {
onCleanStatusTimeCount();
timeCount = setInterval(async () => {
if (!props.state.relationTaskId.value) return;
const res = await props.api.getTaskStatus(props.state.relationTaskId.value);
if (res?.code == 200) {
const { status, progress } = res.data;
if (["error", "success"].includes(status)) {
onCleanTimeCount();
onCleanStatusTimeCount();
props.api?.getTaskList();
// props.api.cleanTaskId();
}
@ -463,7 +463,7 @@ const onModelDirChange = (value: string | null) => {
};
onUnmounted(() => {
onCleanTimeCount();
onCleanStatusTimeCount();
clearLogTimecount();
});

View File

@ -1,17 +1,30 @@
import { ref } from 'vue'
import { taskParamsApi, onInterruptApi, statusApi, logsApi, modelExportApi } from '@/apis/train/index.ts'
import { taskParamsApi, onInterruptApi, statusApi, logsApi, modelExportApi, getExportModelsApi } from '@/apis/train/index.ts'
export const useMain = () => {
// 模型导出
const exportLoading = ref(false)
const modelExport = async (taskId: string) => {
if (exportLoading.value) return false
exportLoading.value = true
const res = await modelExportApi(taskId).catch(err => {
const getExportModelLoading = ref(false)
// 获取可导出的模型
const getExportModels = async (taskId: string) => {
if (getExportModelLoading.value) return false
getExportModelLoading.value = true
const res = await getExportModelsApi(taskId).catch(err => {
return err
})
getExportModelLoading.value = false
return res
}
// 模型导出
const exportLoading = ref(false)
const modelExport = async (data: Record<string, any>) => {
if (exportLoading.value) return false
exportLoading.value = true
const res = await modelExportApi(data).catch(err => {
return err
})
console.log('modelExportApi', res)
exportLoading.value = false
return res
}
@ -84,6 +97,10 @@ export const useMain = () => {
modelExportRequest: {
request: modelExport,
loading: exportLoading
},
getExportModelsRequest: {
loading: getExportModelLoading,
request: getExportModels
}
}
}

View File

@ -6,6 +6,9 @@ export default defineComponent({
</script>
<script setup lang="ts">
import streamSaver from "streamsaver";
import { downloadFile } from "@/utils/download/index.ts";
import _ from "lodash";
import TrainForm from "@/views/modelTraining/detail/components/Form/Train.vue";
import ModuleCard from "@/views/dataManage/index/components/Card/ModuleCard.vue";
@ -41,6 +44,7 @@ const startLogsTimeCount = async (taskId: string) => {
const ends = ["finished", "failed", "stopped"];
if (taskStatus.value?.status && ends.includes(taskStatus.value?.status)) {
onLogCleanUp();
trainDetailServices.task.api.interruptLogs();
return;
}
//
@ -49,6 +53,7 @@ const startLogsTimeCount = async (taskId: string) => {
// trainDetailServices.task.state.logs.value = chunks;
if (done) {
onLogCleanUp();
trainDetailServices.task.api.interruptLogs();
}
}, 2000);
};
@ -84,6 +89,7 @@ const checktaskStatus = async (taskId: string) => {
status = await getLogsFromReader();
if (status) {
onLogCleanUp();
trainDetailServices.task.api.interruptLogs();
}
}, 100);
// //
@ -130,6 +136,7 @@ const init = async () => {
trainDetailServices.options.api.getTrainDataset();
trainDetailServices.options.api.getModels();
const taskId = trainDetailServices.common.state.taskId.value;
trainDetailServices.task.api.getExportModels(taskId);
trainDetailServices.task.api.getTaskParams(taskId);
await Promise.all([trainDetailServices.task.api.getStatus(taskId), getLogsReader(taskId)]);
checktaskStatus(taskId);
@ -172,22 +179,34 @@ const statusColorMap: Record<string, any> = {
//
const showExportResultList = computed(() => {
const options = trainDetailServices.options.state.exportOptions;
const exportList = trainDetailServices.task.state.taskParams.value?.export_formats || [];
const status = taskStatus?.value.status;
const useOptions = options
.filter((item) => {
return exportList.includes(item.value);
const options = trainDetailServices.task.state.validExportModels.value;
const validOptions = options
.filter((item: Record<string, any>) => {
return item.completed;
})
.map((item) => {
.map((item: Record<string, any>) => {
return {
label: `${item.label}`,
value: item.value,
color: status == "finished" ? "green" : "red",
label: item.type.toUpperCase(),
value: item.type,
color: item.completed ? "green" : "red",
};
});
console.log("showExportResultList", useOptions);
return useOptions;
return validOptions;
// const options = trainDetailServices.options.state.exportOptions;
// const exportList = trainDetailServices.task.state.taskParams.value?.export_formats || [];
// const status = taskStatus?.value.status;
// const useOptions = options
// .filter((item) => {
// return exportList.includes(item.value);
// })
// .map((item) => {
// return {
// label: `${item.label}`,
// value: item.value,
// color: status == "finished" ? "green" : "red",
// };
// });
// return useOptions;
});
const onDownload = (info: Record<string, any>) => {
@ -233,9 +252,74 @@ const onInterrupt = () => {
Modal.confirm(modalProps);
};
const onExport = async () => {
const exportLoading = ref(false);
const onExportV2 = async (type: string) => {
const taskId = trainDetailServices.common.state.taskId.value;
const res = await trainDetailServices.task.api.onModelExport(taskId);
const url = `/api/train/${taskId}/export/${type}`;
// a
const modelId = trainDetailServices.task.state.taskParams.value.model_id;
const targetModel = trainDetailServices.options.state.models.value.find(
(item: Record<string, any>) => {
return item.id === modelId;
},
);
const modelName = (targetModel && targetModel?.name) || modelId;
const fileName = `${modelName}.${type.toLowerCase()}`;
console.log("fileName", fileName);
const a = document.createElement("a");
a.href = url;
a.download = fileName;
a.click();
a.remove();
};
const onExport = async (type: string) => {
const url = "";
exportLoading.value = true;
const modelId = trainDetailServices.task.state.taskParams.value.model_id;
const targetModel = trainDetailServices.options.state.models.value.find(
(item: Record<string, any>) => {
return item.id === modelId;
},
);
const modelName = (targetModel && targetModel?.name) || modelId;
const fileName = `${modelName}.${type.toLowerCase()}`;
console.log("fileName", fileName);
const taskId = trainDetailServices.common.state.taskId.value;
const fileStream = streamSaver.createWriteStream(fileName);
await trainDetailServices.task.api.onModelExport(taskId, type).then((res) => {
exportLoading.value = false;
const readableStream = res.body;
if (window.WritableStream && readableStream.pipeTo) {
return readableStream.pipeTo(fileStream).then(() => console.log("完成写入"));
}
// 3
window.writer = fileStream.getWriter();
const reader = res.body.getReader();
const pump = () =>
reader.read().then((res) => (res.done ? writer.close() : writer.write(res.value).then(pump)));
pump();
});
// const readableStream = res.body
// if (window.WritableStream && readableStream.pipeTo) {
// await readableStream.pipeTo(fileSteam)
// // .then(() => console.log(''))
// }
// console.log("res", res);
// const blob = await res.blob();
// console.log("res", res, blob);
// const hrefUrl = window.URL.createObjectURL(blob);
// downloadFile(hrefUrl, `${modelName}.${type}`);
// exportLoading.value = false;
// return res;
};
const onBack = () => {
@ -267,7 +351,7 @@ const prefix = "model-train-page";
</div>
<div :class="`${prefix}-container`">
<div :class="`${prefix}-form`">
<ModuleCard title="模型训练">
<ModuleCard title="模型调优">
<template #head-extra>
<Space :size="20">
<Button
@ -285,11 +369,12 @@ const prefix = "model-train-page";
<Tag v-else :color="statusColorMap[taskStatus?.status]">{{
statusMap[taskStatus?.status]
}}</Tag>
<template v-if="['finished', 'failed', 'stopped'].includes(taskStatus?.status)">
<template v-if="['finished'].includes(taskStatus?.status)">
<Button
@click="onDownload(item)"
@click="onExportV2(item.value)"
type="primary"
v-for="item in showExportResultList"
:loading="exportLoading"
>下载{{ item.label }}</Button
>
<!-- <Tag :color="item.color" v-for="item in showExportResultList">

View File

@ -3,7 +3,7 @@ import { ref } from 'vue'
import { message } from 'ant-design-vue'
export const useMain = () => {
const { taskParamsRequest, interruptRequest, statusRequest, logsRequest, modelExportRequest } = useTaskHooks()
const { taskParamsRequest, interruptRequest, statusRequest, logsRequest, modelExportRequest, getExportModelsRequest } = useTaskHooks()
// 任务参数
const taskParams = ref<any>({})
@ -79,26 +79,41 @@ export const useMain = () => {
}
}
// 导出模型
const onModelExport = async (taskId: string) => {
const res = await modelExportRequest.request(taskId)
console.log('res', res)
// 获取能够导出的模型格式列表
const validExportModels = ref<any[]>([])
const getExportModels = async (taskId: string) => {
const res = await getExportModelsRequest.request(taskId)
if (res?.code == 200) {
const { exported_model_path } = res.data
message.info({
content: `已经导出到${exported_model_path}`,
duration: 10,
});
// todo
validExportModels.value = res.data
}
return res
}
// 导出模型
const onModelExport = async (taskId: string, type: string) => {
const res = await modelExportRequest.request({
taskId,
exportType: type
})
return res
// if (res?.code == 200) {
// const { exported_model_path } = res.data
// message.info({
// content: `已经导出到${exported_model_path}`,
// duration: 10,
// });
// // todo
// }
// return res
}
const state = {
taskParams,
taskStatus,
logs,
logsController
logsController,
validExportModels
}
return {
@ -109,6 +124,7 @@ export const useMain = () => {
getLogs,
interruptLogs,
onInterrupt,
getExportModels,
onModelExport,
clearLogs
},
@ -117,7 +133,8 @@ export const useMain = () => {
interruptRequest,
statusRequest,
logsRequest,
modelExportRequest
modelExportRequest,
getExportModelsRequest
}
}
}

View File

@ -68,7 +68,7 @@ const prefix = "model-form";
:multiple="false"
:before-upload="onUpload"
:max-count="1"
accept=".zip, .rar, .7z, .tgz"
accept=".zip, .tar.gz, .tgz"
>
<div class="upload-box">
<div class="upload-box-icon">
@ -76,7 +76,7 @@ const prefix = "model-form";
</div>
<div class="upload-box-title">点击上传</div>
</div>
<div class="upload-desc">支持.zip, .rar, .7z, .tgz格式</div>
<div class="upload-desc">支持.zip, .tar.gz, .tgz格式</div>
</Upload>
</FormItem>
</Col>

View File

@ -273,22 +273,37 @@ const showUploading = computed(() => {
return listSlice;
})
//
const onUpload = (info: Record<string, any>) => {
//
const onUploadPretrained = (info: Record<string, any>) => {
const { file, row, index } = info;
const params = {
file: file,
modelId: row.id,
};
const formData = new FormData()
formData.append("file", file);
formData.append("modelId", row.id);
uploadingList.value[index] = true
console.log('uploadingList', uploadingList.value)
setTimeout(() => {
props.base.api.importPretrainedForModel(formData).finally(() => {
uploadingList.value[index] = false
}, 2000);
console.log("params", params);
}).then(() => {
getList();
})
return false;
};
const onShowPretrainedDelete = async (info: Record<string, any>) => {
const { row, path } = info
const params = {
modelId: row.id,
path: path
}
const res = await props.base.api.onShowPretrainedDelete(params);
if (res?.code == 200) {
getList();
}
};
//
const init = () => {};
@ -381,13 +396,13 @@ const prefix = "model-manage";
</template>
<!-- 已训练权重集 -->
<template v-if="column.dataIndex == 'tasks'">
<Popover arrow-point-at-center :getPopupContainer="(e: HTMLElement) => props.pageRef">
<Popover arrow-point-at-center :getPopupContainer="(e: HTMLElement) => props.pageRef">
<template #content>
<div class="wrapper">
<div class="wrapper" style="max-height: 600px; overflow: auto;">
<div class="index-box" v-for="(item, index) in record.tasks">
<div class="index-box-circle">{{ index + 1 }}</div>
<div class="index-box-text">{{ item.description }}</div>
</div>
</div>
</template>
@ -400,7 +415,7 @@ const prefix = "model-manage";
>
<Popover arrow-point-at-center :getPopupContainer="(e: HTMLElement) => props.pageRef">
<template #content>
<div class="wrapper">
<div class="wrapper" style="max-height: 600px; overflow: auto;">
<div class="index-box" v-for="(item, index) in text.split(',')">
<div class="index-box-circle">{{ index + 1 }}</div>
<div class="index-box-text">{{ item }}</div>
@ -414,40 +429,50 @@ const prefix = "model-manage";
<template
v-if="['pretrained_weights'].includes(column.dataIndex as string)"
>
<Upload
:multiple="false"
:before-upload="(e: File, fileList: File[]) => onUpload({
file: e,
fileList: fileList,
row: record,
index: index
})"
:disabled="showUploading[index] || false"
:showUploadList="false"
:max-count="1"
style="width: 100%"
accept=".csv"
>
<Button type="primary" size="small" style="display: flex;
align-items: center;
justify-content: center;" :loading="showUploading[index] || false">
<PlusOutlined v-if="!showUploading[index]"/>
<!-- <LoadingOutlined v-else/> -->
</Button>
</Upload>
<div>
<Upload
:multiple="false"
:before-upload="(e: File, fileList: File[]) => onUploadPretrained({
file: e,
fileList: fileList,
row: record,
index: index
})"
:disabled="showUploading[index] || false"
:showUploadList="false"
:max-count="1"
style="width: 100%"
>
<Button
type="primary"
size="small"
style="display: flex; align-items: center; justify-content: center; aspect-ratio: 1; padding: 0"
:loading="showUploading[index] || false">
<PlusOutlined v-if="!showUploading[index]"/>
</Button>
</Upload>
<Popover arrow-point-at-center :getPopupContainer="(e: HTMLElement) => props.pageRef">
<Popover arrow-point-at-center :getPopupContainer="(e: HTMLElement) => props.pageRef" v-if="text.trim().length">
<template #content>
<div class="wrapper">
<div class="index-box" v-for="(item, index) in text.split(',')">
<div class="wrapper" style="max-height: 600px; overflow: auto;">
<div class="index-box" v-for="(item, index) in text.split(',').filter(item => item)">
<div class="index-box-circle">{{ index + 1 }}</div>
<div class="index-box-text">{{ item }}</div>
<div class="index-box-danger" @click="onShowPretrainedDelete({
row: record,
path: item
})">
<Icon name="delete" />
</div>
</div>
</div>
</template>
{{ text }}
<Button type="text" size="small">
<EyeOutlined />
</Button>
</Popover>
</div>
</template>
<!-- 状态 -->
<template v-if="column.dataIndex === 'status'">

View File

@ -1,5 +1,5 @@
import { ref } from 'vue'
import { addApi, updateApi, deleteApi, listApi } from '@/apis/model/index.ts'
import { addApi, updateApi, deleteApi, listApi, importPretrainedForModelApi, deletePretrainedForModelApi } from '@/apis/model/index.ts'
export const useModelHooks = () => {
// 新增
@ -51,6 +51,22 @@ export const useModelHooks = () => {
}
// 导入预训练权重
const importPretrainedForModel = async (data: FormData) => {
const res = await importPretrainedForModelApi(data).catch(err => {
return err
})
return res
}
// 删除预训练权重
const deletePretrainedForModel = async (data: Record<string, any>) => {
const res = await deletePretrainedForModelApi(data).catch(err => {
return err
})
return res
}
return {
addRequest: {
@ -68,6 +84,12 @@ export const useModelHooks = () => {
listRequest: {
request: getModelList,
loading: listLoading
},
importPretrainedForModelRequest: {
request: importPretrainedForModel
},
deletePretrainedForModelRequest: {
request: deletePretrainedForModel
}
}
}

View File

@ -116,12 +116,12 @@ $prefix: "model-manage-page";
:deep(.wrapper) {
display: flex;
gap: 10px;
max-width: 300px;
max-width: 330px;
// width: 470px;
flex-wrap: wrap;
}
:deep(.index-box) {
width: fit-content;
width: 100%;
// width: 150px;
// width: 30px;
height: 30px;
@ -144,6 +144,13 @@ $prefix: "model-manage-page";
line-height: 16px;
font-size: 14px;
}
.index-box-danger {
color: #dd524c;
flex-shrink: 0;
margin-left: auto;
cursor: pointer;
font-size: 14px;
}
}
}
</style>

View File

@ -4,7 +4,7 @@ import { Modal, message } from 'ant-design-vue';
export const useModelServer = () => {
const { listRequest, addRequest, updateRequest, deleteRequest } = useModelHooks()
const { listRequest, addRequest, updateRequest, deleteRequest, importPretrainedForModelRequest, deletePretrainedForModelRequest } = useModelHooks()
const modelList = ref([])
// 获取列表
@ -68,6 +68,35 @@ export const useModelServer = () => {
return res
}
// 导入预训练权重
const importPretrainedForModel = async (data: FormData) => {
const res = await importPretrainedForModelRequest.request(data)
return res
}
// 删除预训练权重
const deletePretrainedForModel = async (data: Record<string, any>) => {
const res = await deletePretrainedForModelRequest.request(data)
return res
}
// 删除预训练权重弹出层
const onShowPretrainedDelete = async (data: Record<string, any>) => {
return new Promise((resolve, reject) => {
const modalProps = createDeleteProps()
modalProps.onOk = async () => {
console.log('onShowPretrainedDelete', data)
const res = await deletePretrainedForModel(data)
if (res?.code == 200) {
message.success('删除成功')
resolve(res)
} else {
reject(res)
}
}
Modal.confirm(modalProps);
})
}
return {
state: {
@ -78,7 +107,10 @@ export const useModelServer = () => {
addModel,
updateModel,
deleteModel,
onShowModelDelete
onShowModelDelete,
importPretrainedForModel,
deletePretrainedForModel,
onShowPretrainedDelete
},
requestMap: {
listRequest,

View File

@ -0,0 +1,436 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ECharts 热图实现</title>
<!-- 引入ECharts -->
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<style>
#chartContainer {
width: 800px;
height: 600px;
margin: 0 auto;
}
</style>
</head>
<body>
<div id="chartContainer"></div>
<script>
// 初始化图表
const myChart = echarts.init(document.getElementById('chartContainer'))
// 响应窗口大小变化
window.addEventListener('resize', function () {
myChart.resize()
})
var graph = {
nodes: [
{
name: 'A',
value: ['教授', '在职', '党委办公室、校长办公室'],
category: '中心教师',
symbolSize: 50,
tooltip: {
formatter: '{b0}:{c0}',
},
},
{
name: 'B',
category: '论文',
value: ['期刊论文', '2005'],
symbolSize: 20,
tooltip: {
formatter: '{b0}:{c0}',
},
},
{
name: 'C',
category: '论文合作教师',
symbolSize: 50,
},
{
name: 'D',
category: '项目',
value: ['信息与通信工程学院', '立项日期20131101'],
symbolSize: 20,
tooltip: {
formatter: '{b0}:{c0}',
},
},
{
name: 'N',
category: '项目合作教师',
symbolSize: 50,
},
{
name: 'Q',
category: '项目',
symbolSize: 20,
},
{
name: 'S',
category: '项目',
symbolSize: 20,
},
{
name: 'E',
category: '专著专利',
symbolSize: 20,
value: ['专利文献', '2009-07-21'],
tooltip: {
formatter: '{b0}:{c0}',
},
},
{
name: 'W',
category: '专著专利',
symbolSize: 20,
},
{
name: 'Y',
category: '专著专利',
symbolSize: 20,
},
{
name: 'Z',
category: '专著专利',
symbolSize: 20,
},
{
name: 'F',
category: '专著专利合作教师',
symbolSize: 50,
},
{
name: 'O',
category: '专著专利合作教师',
symbolSize: 50,
},
{
name: 'P',
category: '专著专利合作教师',
symbolSize: 50,
},
{
value: ['信息与通信工程学院', '信息工程 '],
name: 'G',
category: '课程',
symbolSize: 20,
tooltip: {
formatter: '{b0}:{c0}',
},
},
{
name: 'I',
category: '课程',
symbolSize: 20,
},
{
name: 'L',
category: '课程',
symbolSize: 20,
},
{
name: 'H',
category: '课程合作教师',
symbolSize: 50,
},
{
name: 'M',
category: '课程合作教师',
symbolSize: 50,
},
],
links: [
{
source: 'A',
target: 'B',
},
{
source: 'B',
target: 'C',
},
{
source: 'A',
target: 'D',
},
{
source: 'D',
target: 'N',
},
{
source: 'N',
target: 'Q',
},
{
source: 'N',
target: 'R',
},
{
source: 'A',
target: 'E',
},
{
source: 'E',
target: 'F',
},
{
source: 'F',
target: 'W',
},
{
source: 'F',
target: 'Y',
},
{
source: 'P',
target: 'Z',
},
{
source: 'E',
target: 'O',
},
{
source: 'E',
target: 'P',
},
{
source: 'A',
target: 'G',
},
{
source: 'G',
target: 'H',
},
{
source: 'G',
target: 'M',
},
{
source: 'M',
target: 'I',
},
{
source: 'M',
target: 'L',
},
],
}
const defaultCategory = '中心教师'
const graphTitle = '力导向关系图--实现点击节点展开折叠'
const currentGraph = {
nodes: {},
links: {},
}
const nodeMap = {}
// 页面加载时,第一次初始化图
function init() {
// 根据定义的常量产生currentGraph的默认数据
// 遍历全部nodes和links产生node映射map
for (let i = 0; i < graph.nodes.length; i++) {
if (graph.nodes[i].category === defaultCategory) {
currentGraph.nodes[graph.nodes[i].name] = graph.nodes[i]
}
nodeMap[graph.nodes[i].name] = graph.nodes[i]
nodeMap[graph.nodes[i].name]['links'] = {}
nodeMap[graph.nodes[i].name]['nodes'] = {}
nodeMap[graph.nodes[i].name]['hasAppend'] = false
}
for (let i = 0; i < graph.links.length; i++) {
let link = graph.links[i]
if (
nodeMap[link.source] !== undefined &&
nodeMap[link.target] !== undefined
) {
nodeMap[link.source].links[link.target] = link
nodeMap[link.source].nodes[nodeMap[link.target].name] =
nodeMap[link.target]
}
}
for (let i = 0; i < graph.nodes.length; i++) {
graph.nodes[i].itemStyle = null
graph.nodes[i].label = {
normal: {
show: graph.nodes[i].symbolSize > 15,
},
}
}
redrawGraph()
}
// 处理点击节点展开
function append(nodeName) {
// 根据nodeName从nodeMap里拿出对应的nodes和links并append到currentGraph.nodes currentGraph.links
let node = nodeMap[nodeName]
if (
node.hasAppend === true ||
Object.keys(node.nodes).length === 0 ||
Object.keys(node.links).length === 0
) {
alert('无法继续展开')
return
}
Object.values(node.nodes).forEach((n) => {
currentGraph.nodes[n.name] = n
})
Object.values(node.links).forEach((l) => {
currentGraph.links[l.source + '_' + l.target] = l
})
node.hasAppend = true
redrawGraph()
}
// 处理点击节点收缩
function remove(nodeName) {
//根据nodeName从nodeMap里拿出对应的nodes和links从currentGraph.nodes currentGraph.links删除当前节点的nodes和links并且递归
let node = nodeMap[nodeName]
Object.values(node.nodes).forEach((n) => {
delete currentGraph.nodes[n.name]
if (n.hasAppend === true && Object.keys(n.nodes).length > 0) {
remove(n.name)
}
})
Object.values(node.links).forEach((l) => {
delete currentGraph.links[l.source + '_' + l.target]
})
// 设置flag 等于false
node.hasAppend = false
redrawGraph()
}
// 根据更新后的option重新画图
function redrawGraph() {
option.series[0].data = Object.values(currentGraph.nodes)
option.series[0].links = Object.values(currentGraph.links)
console.log(option)
myChart.setOption(option)
}
const option = {
title: {
text: graphTitle,
top: 'top',
left: 'center',
},
tooltip: {},
legend: [],
animation: false,
series: [
{
type: 'graph',
layout: 'force',
data: Object.values(currentGraph.nodes),
links: Object.values(currentGraph.links),
categories: [
{
name: '中心教师',
itemStyle: {
color: '#c23531',
},
},
{
name: '专著专利合作教师',
itemStyle: {
color: '#749f83',
},
},
{
name: '课程合作教师',
itemStyle: {
color: '#6e7074',
},
},
{
name: '论文合作教师',
itemStyle: {
color: '#2f4554',
},
},
{
name: '论文',
itemStyle: {
color: '#61a0a8',
},
},
{
name: '专著专利',
itemStyle: {
color: '#91c7ae',
},
},
{
name: '课程',
itemStyle: {
color: '#999ea4',
},
},
{
name: '项目合作教师',
itemStyle: {
color: '#DEB887',
},
},
{
name: '项目',
itemStyle: {
color: '#bda29a',
},
},
],
roam: true,
focusNodeAdjacency: false,
itemStyle: {
normal: {
borderColor: '#fff',
borderWidth: 1,
shadowBlur: 10,
shadowColor: 'rgba(0, 0, 0, 0.3)',
},
},
label: {
position: 'right',
formatter: '{b}',
},
lineStyle: {
color: 'target',
opacity: 0.6,
curveness: 0.3,
},
emphasis: {
lineStyle: {
width: 10,
},
},
force: {
layoutAnimation: false,
repulsion: 500,
},
},
],
}
init()
// 设置配置项
myChart.setOption(option)
myChart.on('click', function (params) {
if (params.dataType === 'node') {
const node = nodeMap[params.data.name]
if (node.hasAppend === true) {
remove(node.name)
} else {
append(node.name)
}
}
})
</script>
</body>
</html>

View File

@ -68,6 +68,7 @@
// 生成数据
const { times, yValues, data } = generateData();
console.log('data', data)
// 配置选项
const option = {

View File

@ -0,0 +1,61 @@
// knowledge-graph.data.ts
export const f35Data = {
nodes: [
// 中心节点 - F35战斗机
{
id: 'f35',
name: 'F-35闪电II战斗机',
type: 'aircraft',
description: '第五代隐形多用途战斗机',
color: '#4fc3f7',
image: '/images/f35-icon.png',
size: 80
},
// 国家节点
{ id: 'usa', name: '美国', type: 'country', color: '#1e88e5', size: 40 },
{ id: 'uk', name: '英国', type: 'country', color: '#f44336', size: 35 },
{ id: 'japan', name: '日本', type: 'country', color: '#e53935', size: 35 },
{ id: 'israel', name: '以色列', type: 'country', color: '#43a047', size: 30 },
// 军事单位节点
{ id: 'usaf', name: '美国空军', type: 'military', color: '#5e35b1', size: 25 },
{ id: 'raf', name: '皇家空军', type: 'military', color: '#7b1fa2', size: 25 },
{ id: 'jasdf', name: '日本空中自卫队', type: 'military', color: '#d81b60', size: 25 },
// 武器装备节点
{ id: 'f135', name: 'F135发动机', type: 'technology', color: '#ffb300', size: 30 },
{ id: 'meteor', name: '流星导弹', type: 'weapon', color: '#e65100', size: 28 },
{ id: 'jdam', name: 'B16-12核弹', type: 'weapon', color: '#bf360c', size: 28 },
{ id: 'spear3', name: '长矛-3导弹', type: 'weapon', color: '#5d4037', size: 28 },
// 航母节点
{ id: 'ford', name: '福特级核动力航母', type: 'technology', color: '#00695c', size: 35 },
{ id: 'america', name: '美利坚号航母', type: 'technology', color: '#004d40', size: 32 },
{ id: 'queen', name: '伊丽莎白女王号航母', type: 'technology', color: '#827717', size: 32 }
],
links: [
// F35相关连接
{ source: 'f35', target: 'usa', label: '研制国', strength: 1.0 },
{ source: 'f35', target: 'f135', label: '使用发动机', strength: 0.9 },
{ source: 'f35', target: 'meteor', label: '配备武器', strength: 0.8 },
{ source: 'f35', target: 'jdam', label: '配备武器', strength: 0.8 },
{ source: 'f35', target: 'spear3', label: '配备武器', strength: 0.8 },
// 国家与军事单位
{ source: 'usa', target: 'usaf', label: '所属', strength: 0.9 },
{ source: 'uk', target: 'raf', label: '所属', strength: 0.9 },
{ source: 'japan', target: 'jasdf', label: '所属', strength: 0.9 },
// 部署关系
{ source: 'usaf', target: 'f35', label: '装备', strength: 0.7 },
{ source: 'raf', target: 'f35', label: '装备', strength: 0.7 },
{ source: 'jasdf', target: 'f35', label: '装备', strength: 0.7 },
// 航母部署
{ source: 'ford', target: 'f35', label: '搭载', strength: 0.6 },
{ source: 'america', target: 'f35', label: '搭载', strength: 0.6 },
{ source: 'queen', target: 'f35', label: '搭载', strength: 0.6 }
]
}

View File

@ -0,0 +1,298 @@
<template>
<div class="knowledge-graph-container">
<div class="graph-controls">
<button @click="resetLayout" class="control-btn">重置布局</button>
<button @click="zoomIn" class="control-btn">放大</button>
<button @click="zoomOut" class="control-btn">缩小</button>
<input v-model="searchTerm" placeholder="搜索节点..." class="search-input" />
</div>
<svg ref="svgElement" class="knowledge-graph" @click="handleNodeClick"></svg>
<div v-if="selectedNode" class="node-info-panel">
<h3>{{ selectedNode.name }}</h3>
<p class="node-type">分类: {{ getTypeLabel(selectedNode.type) }}</p>
<p v-if="selectedNode.description" class="node-description">{{ selectedNode.description }}</p>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { KnowledgeGraph, GraphNode } from "./logic";
//
const svgElement = ref<SVGSVGElement>();
const searchTerm = ref("");
const selectedNode = ref<GraphNode | null>(null);
let knowledgeGraph: KnowledgeGraph;
/**
* 放大功能实现
*/
const zoomIn = () => {
if (knowledgeGraph) {
knowledgeGraph.zoomIn();
}
};
/**
* 缩小功能实现
*/
const zoomOut = () => {
if (knowledgeGraph) {
knowledgeGraph.zoomOut();
}
};
/**
* 重置布局功能实现
*/
const resetLayout = () => {
if (knowledgeGraph) {
knowledgeGraph.resetLayout();
}
};
/**
* 处理节点点击事件
* @param event 点击事件
*/
const handleNodeClick = (event: MouseEvent) => {
const target = event.target as SVGElement;
//
const nodeElement = target.closest(".node");
if (nodeElement) {
const nodeId = nodeElement.getAttribute("data-node-id");
if (nodeId && knowledgeGraph) {
const node = knowledgeGraph.getNodeInfo(nodeId);
if (node) {
selectedNode.value = node;
}
}
}
};
/**
* 获取节点类型的中文标签
* @param type 节点类型
* @returns 中文类型标签
*/
const getTypeLabel = (type: string): string => {
const typeMap: Record<string, string> = {
aircraft: "飞机",
country: "国家",
weapon: "武器",
military: "军事单位",
technology: "技术/装备",
};
return typeMap[type] || type;
};
onMounted(() => {
if (svgElement.value) {
knowledgeGraph = new KnowledgeGraph(svgElement.value);
//
setTimeout(() => {
const nodeElements = svgElement.value?.querySelectorAll(".node");
if (nodeElements) {
nodeElements.forEach((element, index) => {
// data
const nodeData = (knowledgeGraph as any).getNodeInfoByIndex?.(index) || {
id: `node-${index}`,
};
element.setAttribute("data-node-id", nodeData.id);
});
}
}, 100);
}
});
</script>
<style scoped lang="scss">
.knowledge-graph-container {
width: 100%;
height: 100vh;
background: radial-gradient(ellipse at center, #0c2461 0%, #000 100%);
position: relative;
overflow: hidden;
}
.knowledge-graph {
width: 100%;
height: 100%;
cursor: pointer;
:deep(.node) {
cursor: pointer;
transition: all 0.3s ease;
&:hover {
filter: drop-shadow(0 0 10px currentColor);
// transform: scale(1.05);
}
&:active {
transform: scale(0.95);
}
}
:deep(.link) {
transition: all 0.3s ease;
/* 移除虚线动画,使用实线效果 */
}
:deep(.link:hover) {
stroke-opacity: 1;
// stroke-width: function(d) {(d.level || 0) + 3};
}
:deep(.link-label) {
background: rgba(0, 0, 0, 0.7);
border-radius: 4px;
padding: 2px 6px;
white-space: nowrap;
font-size: 11px;
font-weight: 500;
pointer-events: none;
}
:deep(text) {
font-family: "Arial", "Microsoft YaHei", sans-serif;
font-weight: bold;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
}
:deep(image) {
border-radius: 50%;
object-fit: contain;
background: rgba(255, 255, 255, 0.1);
}
}
.graph-controls {
position: absolute;
top: 20px;
right: 20px;
z-index: 1000;
background: rgba(255, 255, 255, 0.1);
padding: 15px;
border-radius: 8px;
backdrop-filter: blur(10px);
display: flex;
gap: 10px;
align-items: center;
}
.control-btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
background: #2196f3;
color: white;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
&:hover {
background: #1976d2;
// transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
&:active {
transform: translateY(0);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
}
.search-input {
padding: 8px 16px;
border: none;
border-radius: 4px;
background: rgba(255, 255, 255, 0.9);
color: #333;
font-size: 14px;
outline: none;
transition: all 0.3s ease;
&:focus {
background: rgba(255, 255, 255, 1);
box-shadow: 0 0 0 3px rgba(33, 150, 243, 0.3);
}
}
.node-info-panel {
position: absolute;
bottom: 20px;
left: 20px;
background: rgba(255, 255, 255, 0.9);
padding: 20px;
border-radius: 8px;
max-width: 300px;
backdrop-filter: blur(10px);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
border: 1px solid rgba(255, 255, 255, 0.2);
animation: slideIn 0.3s ease-out;
}
.node-info-panel h3 {
margin: 0 0 10px 0;
color: #263238;
font-size: 18px;
font-weight: 600;
}
.node-type {
margin: 0 0 10px 0;
color: #546e7a;
font-size: 14px;
font-weight: 500;
}
.node-description {
margin: 0;
color: #455a64;
font-size: 14px;
line-height: 1.5;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.knowledge-graph-container {
height: 70vh;
}
.graph-controls {
top: 10px;
right: 10px;
left: 10px;
flex-direction: column;
gap: 8px;
.control-btn,
.search-input {
width: 100%;
margin: 0;
}
}
.node-info-panel {
left: 10px;
right: 10px;
max-width: none;
margin: 0;
}
}
</style>

View File

@ -0,0 +1,155 @@
// knowledge-graph.ts
import { f35Data } from './data'
import * as d3 from "d3";
interface GraphNode {
id: string
name: string
type: 'aircraft' | 'country' | 'weapon' | 'military' | 'technology'
description?: string
image?: string
color?: string
}
interface GraphLink {
source: string
target: string
label: string
strength?: number
}
/**
*
*/
export class KnowledgeGraph {
private svg: d3.Selection<SVGSVGElement, unknown, null, undefined>
private simulation: d3.Simulation<GraphNode, GraphLink>
private zoom: d3.ZoomBehavior<SVGSVGElement, unknown>
/**
*
* @param container SVG容器元素
*/
constructor(container: SVGSVGElement) {
this.initSVG(container)
this.initSimulation()
this.initZoom()
this.renderGraph()
}
private initSVG(container: SVGSVGElement) {
const width = 1200, height = 800
this.svg = d3.select(container)
.attr('width', width)
.attr('height', height)
.attr('viewBox', [0, 0, width, height])
}
private initSimulation() {
this.simulation = d3.forceSimulation<GraphNode>(f35Data.nodes)
.force('link', d3.forceLink<GraphNode, GraphLink>(f35Data.links).id(d => d.id).distance(100))
.force('charge', d3.forceManyBody().strength(-300))
.force('center', d3.forceCenter(600, 400))
.force('collision', d3.forceCollide().radius(d => (d as any).size + 10))
}
private initZoom() {
this.zoom = d3.zoom<SVGSVGElement, unknown>()
.scaleExtent([0.1, 4])
.on('zoom', (event) => {
this.svg.selectAll('g.container').attr('transform', event.transform)
})
this.svg.call(this.zoom)
}
private renderGraph() {
const container = this.svg.append('g').attr('class', 'container')
// 绘制连线
const links = container.append('g')
.selectAll('line')
.data(f35Data.links)
.join('line')
.attr('class', 'link')
.style('stroke', '#4CAF50')
.style('stroke-width', 2)
.style('stroke-opacity', 0.6)
// 绘制节点
const nodes = container.append('g')
.selectAll('g.node')
.data(f35Data.nodes)
.join('g')
.attr('class', 'node')
.attr('data-node-id', d => d.id) // 直接添加data-node-id属性
.call(d3.drag<SVGGElement, GraphNode>()
.on('start', this.dragStarted.bind(this))
.on('drag', this.dragged.bind(this))
.on('end', this.dragEnded.bind(this))
)
// 节点圆形
nodes.append('circle')
.attr('r', d => d.size || 20)
.style('fill', d => d.color || '#ccc')
.style('stroke', '#fff')
.style('stroke-width', 2)
// 节点文字
nodes.append('text')
.text(d => d.name)
.attr('text-anchor', 'middle')
.attr('dy', -15)
.style('fill', '#fff')
.style('font-size', '12px')
.style('pointer-events', 'none')
// 力导向模拟更新
this.simulation.on('tick', () => {
links
.attr('x1', d => (d.source as any).x)
.attr('y1', d => (d.source as any).y)
.attr('x2', d => (d.target as any).x)
.attr('y2', d => (d.target as any).y)
nodes.attr('transform', d => `translate(${d.x},${d.y})`)
})
}
private dragStarted(event: any, d: GraphNode) {
if (!event.active) this.simulation.alphaTarget(0.3).restart()
d.fx = d.x
d.fy = d.y
}
private dragged(event: any, d: GraphNode) {
d.fx = event.x
d.fy = event.y
}
private dragEnded(event: any, d: GraphNode) {
if (!event.active) this.simulation.alphaTarget(0)
d.fx = null
d.fy = null
}
/**
*
* @param nodeId ID
* @returns
*/
public getNodeInfo(nodeId: string): GraphNode | undefined {
return f35Data.nodes.find(node => node.id === nodeId)
}
/**
*
* @param index
* @returns
*/
public getNodeInfoByIndex(index: number): GraphNode | undefined {
return f35Data.nodes[index]
}
}

View File

@ -6,7 +6,8 @@ export default defineComponent({
</script>
<script setup lang="ts">
import chart from "@/views/test/components/chart/t.vue";
// import chart from "@/views/test/components/chart/t.vue";
import chart from "@/views/test/components/knowledgeChat/index.vue";
</script>
<template>

View File

@ -23,7 +23,7 @@ export default defineConfig({
server: {
host: true,
port: 3019, // 指定端口
strictPort: true, // 如果端口已被占用则退出
strictPort: false, // 如果端口已被占用则退出
proxy: {
'/api': {
target: URL_USE,